Compare commits

..

No commits in common. "main" and "0.19.0-rc.7" have entirely different histories.

471 changed files with 12230 additions and 23977 deletions

2
.cargo/config Normal file
View file

@ -0,0 +1,2 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]

4
.github/CODEOWNERS vendored
View file

@ -1,3 +1,3 @@
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917 * @Nutomic @dessalines @phiresky
crates/apub/ @Nutomic crates/apub/ @Nutomic
migrations/ @dessalines @phiresky @dullbananas migrations/ @dessalines @phiresky

View file

@ -20,8 +20,6 @@ body:
required: true required: true
- label: Is this only a single bug? Do not put multiple bugs in one issue. - label: Is this only a single bug? Do not put multiple bugs in one issue.
required: true required: true
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
required: true
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues. - label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
required: true required: true
- type: textarea - type: textarea

View file

@ -20,8 +20,6 @@ body:
required: true required: true
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues. - label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
required: true required: true
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
required: true
- type: textarea - type: textarea
id: problem id: problem
attributes: attributes:

1
.gitignore vendored
View file

@ -20,6 +20,7 @@ query_testing/**/reports/*.json
api_tests/node_modules api_tests/node_modules
api_tests/.yalc api_tests/.yalc
api_tests/yalc.lock api_tests/yalc.lock
api_tests/test.png
api_tests/pict-rs api_tests/pict-rs
# pictrs data # pictrs data

View file

@ -3,5 +3,3 @@ edition = "2021"
imports_layout = "HorizontalVertical" imports_layout = "HorizontalVertical"
imports_granularity = "Crate" imports_granularity = "Crate"
group_imports = "One" group_imports = "One"
wrap_comments = true
comment_width = 100

View file

@ -2,40 +2,32 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677 # See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables: variables:
- &rust_image "rust:1.78" - &rust_image "rust:1.74.0"
- &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "corepack enable pnpm"
- &slow_check_paths - &slow_check_paths
- event: pull_request - path:
path:
include: [
# rust source code # rust source code
"crates/**", - "**/*.rs"
"src/**", - "**/Cargo.toml"
"**/Cargo.toml", - "Cargo.lock"
"Cargo.lock",
# database migrations # database migrations
"migrations/**", - "migrations/**"
# typescript tests # typescript tests
"api_tests/**", - "api_tests/**"
# config files and scripts used by ci # config files and scripts used by ci
".woodpecker.yml", - ".woodpecker.yml"
".rustfmt.toml", - ".rustfmt.toml"
"scripts/update_config_defaults.sh", - "scripts/update_config_defaults.sh"
"diesel.toml", - "diesel.toml"
".gitmodules", - ".gitmodules"
]
- install_binstall: &install_binstall # Broken for cron jobs currently, see
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz # https://github.com/woodpecker-ci/woodpecker/issues/1716
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz # clone:
- cp cargo-binstall /usr/local/cargo/bin # git:
- install_diesel_cli: &install_diesel_cli # image: woodpeckerci/plugin-git
- apt update && apt install -y lsb-release build-essential # settings:
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' # recursive: true
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - # submodule_update_remote: true
- apt update && apt install -y postgresql-client-16
- cargo install diesel_cli --no-default-features --features postgres
- export PATH="$CARGO_HOME/bin:$PATH"
steps: steps:
prepare_repo: prepare_repo:
@ -44,64 +36,74 @@ steps:
- apk add git - apk add git
- git submodule init - git submodule init
- git submodule update - git submodule update
when:
- event: [pull_request, tag]
prettier_check: prettier_check:
group: format
image: tmknom/prettier:3.0.0 image: tmknom/prettier:3.0.0
commands: commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml' - prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations'
when:
- event: pull_request
toml_fmt: toml_fmt:
group: format
image: tamasfe/taplo:0.8.1 image: tamasfe/taplo:0.8.1
commands: commands:
- taplo format --check - taplo format --check
when:
- event: pull_request
sql_fmt: sql_fmt:
image: backplane/pgformatter group: format
image: backplane/pgformatter:latest
commands: commands:
- ./scripts/sql_format_check.sh - ./scripts/sql_format_check.sh
when:
- event: pull_request
cargo_fmt: cargo_fmt:
image: *rust_nightly_image group: format
image: rustlang/rust:nightly
environment: environment:
# store cargo data in repo folder so that it gets cached between steps # store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- rustup component add rustfmt # need make existing toolchain available
- cargo +nightly fmt -- --check - cargo +nightly fmt -- --check
when:
- event: pull_request
cargo_machete: cargo_machete:
image: *rust_nightly_image group: format
image: rustlang/rust:nightly
commands: commands:
- <<: *install_binstall - wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
- cp cargo-binstall /usr/local/cargo/bin
- cargo binstall -y cargo-machete - cargo binstall -y cargo-machete
- cargo machete - cargo machete
when:
- event: pull_request
ignored_files: restore-cache:
image: alpine:3 image: meltwater/drone-cache:v1
commands: pull: true
- apk add git settings:
- IGNORED=$(git ls-files --cached -i --exclude-standard) restore: true
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi endpoint:
when: from_secret: MINIO_ENDPOINT
- event: pull_request access-key:
from_secret: MINIO_WRITE_USER
secret-key:
from_secret: MINIO_WRITE_PASSWORD
bucket:
from_secret: MINIO_BUCKET
region: us-east-1
cache_key: "rust-cache"
path-style: true
mount:
- ".cargo"
- "target"
- "api_tests/node_modules"
secrets:
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
when: *slow_check_paths
# make sure api builds with default features (used by other crates relying on lemmy api) # make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features: check_api_common_default_features:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- cargo check --package lemmy_api_common - cargo check --package lemmy_api_common
when: *slow_check_paths when: *slow_check_paths
@ -109,7 +111,7 @@ steps:
lemmy_api_common_doesnt_depend_on_diesel: lemmy_api_common_doesnt_depend_on_diesel:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel" - "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
when: *slow_check_paths when: *slow_check_paths
@ -117,7 +119,7 @@ steps:
lemmy_api_common_works_with_wasm: lemmy_api_common_works_with_wasm:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- "rustup target add wasm32-unknown-unknown" - "rustup target add wasm32-unknown-unknown"
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common" - "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
@ -126,7 +128,7 @@ steps:
check_defaults_hjson_updated: check_defaults_hjson_updated:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- export LEMMY_CONFIG_LOCATION=./config/config.hjson - 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
@ -134,34 +136,32 @@ steps:
when: *slow_check_paths when: *slow_check_paths
check_diesel_schema: check_diesel_schema:
image: *rust_image image: willsquire/diesel-cli
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands: commands:
- <<: *install_diesel_cli
- diesel migration run - diesel migration run
- diesel print-schema --config-file=diesel.toml > tmp.schema - diesel print-schema --config-file=diesel.toml > tmp.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
check_db_perf_tool: check_diesel_migration_revertable:
image: *rust_image image: willsquire/diesel-cli
environment: environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy CARGO_HOME: .cargo
RUST_BACKTRACE: "1" DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
CARGO_HOME: .cargo_home
commands: commands:
# same as scripts/db_perf.sh but without creating a new database server - diesel migration run
- export LEMMY_CONFIG_LOCATION=config/config.hjson - diesel migration redo
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths when: *slow_check_paths
cargo_clippy: cargo_clippy:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
# when adding new clippy lints, make sure to also add them in scripts/lint.sh
- rustup component add clippy - rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings - cargo clippy --workspace --tests --all-targets --features console -- -D warnings
when: *slow_check_paths when: *slow_check_paths
@ -169,84 +169,63 @@ steps:
cargo_build: cargo_build:
image: *rust_image image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- cargo build - cargo build
- mv target/debug/lemmy_server target/lemmy_server - mv target/debug/lemmy_server target/lemmy_server
when: *slow_check_paths when: *slow_check_paths
cargo_test: cargo_test:
group: tests
image: *rust_image image: *rust_image
environment: environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1" RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home CARGO_HOME: .cargo
commands: commands:
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson - 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_diesel_migration:
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
PGUSER: lemmy
PGPASSWORD: password
PGHOST: database
PGDATABASE: lemmy
commands:
# Install diesel_cli
- <<: *install_diesel_cli
# Run all migrations
- diesel migration run
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
- apt update && apt install -y lsb-release
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
- apt update && apt install -y postgresql-client-16
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
# Make sure that the newest migration is revertable without the `r` schema
- diesel migration redo
# Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things
- alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending"
- lemmy_schema_setup
- lemmy_schema_setup
# Make sure that the newest migration is revertable with the `r` schema
- diesel migration redo
# Check for changes in the schema, which would be caused by an incorrect migration
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump
- diff before.sqldump after.sqldump
when: *slow_check_paths
run_federation_tests: run_federation_tests:
group: tests
image: node:20-bookworm-slim image: node:20-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"
commands: commands:
- *install_pnpm
- apt update && apt install -y bash curl postgresql-client - apt update && apt install -y bash curl postgresql-client
- bash api_tests/prepare-drone-federation-test.sh - bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/ - cd api_tests/
- pnpm i - yarn
- pnpm api-test - yarn api-test
when: *slow_check_paths when: *slow_check_paths
federation_tests_server_output: rebuild-cache:
image: alpine:3 image: meltwater/drone-cache:v1
commands: pull: true
# `|| true` prevents this step from appearing to fail if the server output files don't exist settings:
- cat target/log/lemmy_*.out || true rebuild: true
- "# If you can't see all output, then use the download button" endpoint:
from_secret: MINIO_ENDPOINT
access-key:
from_secret: MINIO_WRITE_USER
secret-key:
from_secret: MINIO_WRITE_PASSWORD
bucket:
from_secret: MINIO_BUCKET
cache_key: "rust-cache"
region: us-east-1
path-style: true
mount:
- ".cargo"
- "target"
- "api_tests/node_modules"
secrets:
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
when: when:
- event: pull_request - event: push
status: failure branch: main
publish_release_docker: publish_release_docker:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
@ -254,12 +233,14 @@ steps:
settings: settings:
repo: dessalines/lemmy repo: dessalines/lemmy
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
platforms: linux/amd64, linux/arm64 # TODO fix arm build: see: https://woodpecker.join-lemmy.org/repos/129/pipeline/2888/20
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64
build_args: build_args:
- RUST_RELEASE_MODE=release - RUST_RELEASE_MODE=release
tag: ${CI_COMMIT_TAG} tag: ${CI_COMMIT_TAG}
when: when:
- event: tag event: tag
nightly_build: nightly_build:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
@ -272,20 +253,7 @@ steps:
- RUST_RELEASE_MODE=release - RUST_RELEASE_MODE=release
tag: dev tag: dev
when: when:
- event: cron event: cron
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
image: *rust_image
commands:
- <<: *install_binstall
# Install cargo-workspaces
- cargo binstall -y cargo-workspaces
- cp -r migrations crates/db_schema/
- cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
- event: tag
notify_on_failure: notify_on_failure:
image: alpine:3 image: alpine:3
@ -293,8 +261,7 @@ steps:
- apk add curl - apk add curl
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci" - "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when: when:
- event: [pull_request, tag] status: [failure]
status: failure
notify_on_tag_deploy: notify_on_tag_deploy:
image: alpine:3 image: alpine:3
@ -302,11 +269,11 @@ steps:
- apk add curl - apk add curl
- "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci" - "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
when: when:
- event: tag event: tag
services: services:
database: database:
image: postgres:16-alpine image: postgres:15.2-alpine
environment: environment:
POSTGRES_USER: lemmy POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password

3
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,3 @@
# Contributing
See [here](https://join-lemmy.org/docs/en/contributors/01-overview.html) for contributing Instructions.

3791
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.5" version = "0.19.0-rc.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"
@ -16,7 +16,6 @@ license.workspace = true
homepage.workspace = true homepage.workspace = true
documentation.workspace = true documentation.workspace = true
repository.workspace = true repository.workspace = true
publish = false
[lib] [lib]
doctest = false doctest = false
@ -37,8 +36,6 @@ debug = 0
[features] [features]
embed-pictrs = ["pict-rs"] embed-pictrs = ["pict-rs"]
# This feature requires building with `tokio_unstable` flag, see documentation:
# https://docs.rs/tokio/latest/tokio/#unstable-features
console = [ console = [
"console-subscriber", "console-subscriber",
"opentelemetry", "opentelemetry",
@ -56,7 +53,6 @@ members = [
"crates/api_common", "crates/api_common",
"crates/apub", "crates/apub",
"crates/utils", "crates/utils",
"crates/db_perf",
"crates/db_schema", "crates/db_schema",
"crates/db_views", "crates/db_views",
"crates/db_views_actor", "crates/db_views_actor",
@ -67,8 +63,8 @@ members = [
[workspace.lints.clippy] [workspace.lints.clippy]
cast_lossless = "deny" cast_lossless = "deny"
complexity = { level = "deny", priority = -1 } complexity = "deny"
correctness = { level = "deny", priority = -1 } correctness = "deny"
dbg_macro = "deny" dbg_macro = "deny"
explicit_into_iter_loop = "deny" explicit_into_iter_loop = "deny"
explicit_iter_loop = "deny" explicit_iter_loop = "deny"
@ -79,95 +75,85 @@ inefficient_to_string = "deny"
items-after-statements = "deny" items-after-statements = "deny"
manual_string_new = "deny" manual_string_new = "deny"
needless_collect = "deny" needless_collect = "deny"
perf = { level = "deny", priority = -1 } perf = "deny"
redundant_closure_for_method_calls = "deny" redundant_closure_for_method_calls = "deny"
style = { level = "deny", priority = -1 } style = "deny"
suspicious = { level = "deny", priority = -1 } suspicious = "deny"
uninlined_format_args = "allow" uninlined_format_args = "allow"
unused_self = "deny" unused_self = "deny"
unwrap_used = "deny" unwrap_used = "deny"
[workspace.dependencies] [workspace.dependencies]
lemmy_api = { version = "=0.19.5", path = "./crates/api" } lemmy_api = { version = "=0.19.0-rc.7", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.5", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.0-rc.7", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.5", path = "./crates/apub" } lemmy_apub = { version = "=0.19.0-rc.7", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.5", path = "./crates/utils", default-features = false } lemmy_utils = { version = "=0.19.0-rc.7", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.5", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.0-rc.7", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.5", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.0-rc.7", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.5", path = "./crates/routes" } lemmy_routes = { version = "=0.19.0-rc.7", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" } lemmy_db_views = { version = "=0.19.0-rc.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.0-rc.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.0-rc.7", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.5", path = "./crates/federate" } activitypub_federation = { version = "0.5.0-beta.5", default-features = false, features = [
activitypub_federation = { version = "0.5.6", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
diesel = "2.1.6" diesel = "2.1.3"
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
diesel-async = "0.4.1" diesel-async = "0.3.2"
serde = { version = "1.0.203", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
serde_with = "3.8.1" serde_with = "3.4.0"
actix-web = { version = "4.6.0", default-features = false, features = [ actix-web = { version = "4.4.0", default-features = false, features = [
"macros", "macros",
"rustls-0_23", "rustls",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
"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.8", default-features = false }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-log = "0.2.0" tracing-log = "0.1.4"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] } reqwest = { version = "0.11.22", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.5" reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.8" reqwest-tracing = "0.4.6"
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.0"
chrono = { version = "0.4.38", features = ["serde"], default-features = false } chrono = { version = "0.4.31", features = ["serde"], default-features = false }
serde_json = { version = "1.0.117", features = ["preserve_order"] } serde_json = { version = "1.0.107", features = ["preserve_order"] }
base64 = "0.22.1" base64 = "0.21.5"
uuid = { version = "1.8.0", features = ["serde", "v4"] } uuid = { version = "1.5.0", features = ["serde", "v4"] }
async-trait = "0.1.80" async-trait = "0.1.74"
captcha = "0.0.9" captcha = "0.0.9"
anyhow = { version = "1.0.86", features = [ anyhow = { version = "1.0.75", 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.0"
typed-builder = "0.18.2" typed-builder = "0.15.2"
serial_test = "3.1.1" serial_test = "2.0.0"
tokio = { version = "1.38.0", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }
regex = "1.10.4" regex = "1.10.2"
once_cell = "1.19.0" once_cell = "1.18.0"
diesel-derive-newtype = "2.1.2" diesel-derive-newtype = "2.1.0"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.26.2" strum = "0.25.0"
strum_macros = "0.26.4" strum_macros = "0.25.3"
itertools = "0.13.0" itertools = "0.11.0"
futures = "0.3.30" futures = "0.3.28"
http = "0.2.12" http = "0.2.9"
percent-encoding = "2.3.0"
rosetta-i18n = "0.1.3" rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" } tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.1.1", features = [ ts-rs = { version = "7.0.0", features = ["serde-compat", "chrono-impl"] }
"serde-compat", rustls = { version = "0.21.8", features = ["dangerous_configuration"] }
"chrono-impl", futures-util = "0.3.28"
"no-serde-warnings",
] }
rustls = { version = "0.23.9", features = ["ring"] }
futures-util = "0.3.30"
tokio-postgres = "0.7.10" tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.12.0" tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.7" enum-map = "2.7"
moka = { version = "0.12.7", features = ["future"] }
i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.6", features = ["derive", "env"] }
pretty_assertions = "1.4.0"
derive-new = "0.6.0"
[dependencies] [dependencies]
lemmy_api = { workspace = true } lemmy_api = { workspace = true }
@ -177,7 +163,7 @@ lemmy_utils = { workspace = true }
lemmy_db_schema = { workspace = true } lemmy_db_schema = { workspace = true }
lemmy_api_common = { workspace = true } lemmy_api_common = { workspace = true }
lemmy_routes = { workspace = true } lemmy_routes = { workspace = true }
lemmy_federate = { workspace = true } lemmy_federate = { version = "0.19.0-rc.7", path = "crates/federate" }
activitypub_federation = { workspace = true } activitypub_federation = { workspace = true }
diesel = { workspace = true } diesel = { workspace = true }
diesel-async = { workspace = true } diesel-async = { workspace = true }
@ -195,17 +181,13 @@ clokwerk = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true } tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.3.0", optional = true } console-subscriber = { version = "0.1.10", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.15", optional = true } pict-rs = { version = "0.4.5", optional = true }
tokio.workspace = true tokio.workspace = true
actix-cors = "0.7.0" actix-cors = "0.6.4"
futures-util = { workspace = true } futures-util = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
prometheus = { version = "0.13.4", features = ["process"] } prometheus = { version = "0.13.3", features = ["process"] }
serial_test = { workspace = true } serial_test = { workspace = true }
clap = { workspace = true } clap = { version = "4.4.7", features = ["derive"] }
actix-web-prom = "0.8.0"
[dev-dependencies]
pretty_assertions = { workspace = true }

View file

@ -7,7 +7,7 @@
[![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/) [![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE) [![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social) ![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
<a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a> [![Delightful Humane Tech](https://codeberg.org/teaserbot-labs/delightful-humane-design/raw/branch/main/humane-tech-badge.svg)](https://codeberg.org/teaserbot-labs/delightful-humane-design)
</div> </div>
@ -107,6 +107,7 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
- NSFW post / community support. - NSFW post / community support.
- High performance. - High performance.
- Server is written in rust. - Server is written in rust.
- Front end is `~80kB` gzipped.
- Supports arm64 / Raspberry Pi. - Supports arm64 / Raspberry Pi.
## Installation ## Installation
@ -121,8 +122,6 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project. Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
Lemmy is made possible by a generous grant from the [NLnet foundation](https://nlnet.nl/).
- [Support on Liberapay](https://liberapay.com/Lemmy). - [Support on Liberapay](https://liberapay.com/Lemmy).
- [Support on Patreon](https://www.patreon.com/dessalines). - [Support on Patreon](https://www.patreon.com/dessalines).
- [Support on OpenCollective](https://opencollective.com/lemmy). - [Support on OpenCollective](https://opencollective.com/lemmy).
@ -133,25 +132,21 @@ Lemmy is made possible by a generous grant from the [NLnet foundation](https://n
- bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK` - bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01` - ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV` - monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
- cardano: `addr1q858t89l2ym6xmrugjs0af9cslfwvnvsh2xxp6x4dcez7pf5tushkp4wl7zxfhm2djp6gq60dk4cmc7seaza5p3slx0sakjutm`
## Contributing ## Contributing
Read the following documentation to setup the development environment and start coding:
- [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.html) - [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.html)
- [Docker Development](https://join-lemmy.org/docs/contributors/03-docker-development.html) - [Docker Development](https://join-lemmy.org/docs/contributors/03-docker-development.html)
- [Local Development](https://join-lemmy.org/docs/contributors/02-local-development.html) - [Local Development](https://join-lemmy.org/docs/contributors/02-local-development.html)
When working on an issue or pull request, you can comment with any questions you may have so that maintainers can answer them. You can also join the [Matrix Development Chat](https://matrix.to/#/#lemmydev:matrix.org) for general assistance.
### Translations ### Translations
- If you want to help with translating, take a look at [Weblate](https://weblate.join-lemmy.org/projects/lemmy/). You can also help by [translating the documentation](https://github.com/LemmyNet/lemmy-docs#adding-a-new-language). - If you want to help with translating, take a look at [Weblate](https://weblate.join-lemmy.org/projects/lemmy/). You can also help by [translating the documentation](https://github.com/LemmyNet/lemmy-docs#adding-a-new-language).
## Community ## Contact
- [Matrix Space](https://matrix.to/#/#lemmy-space:matrix.org) - [Mastodon](https://mastodon.social/@LemmyDev)
- [Lemmy Forum](https://lemmy.ml/c/lemmy)
- [Lemmy Support Forum](https://lemmy.ml/c/lemmy_support) - [Lemmy Support Forum](https://lemmy.ml/c/lemmy_support)
## Code Mirrors ## Code Mirrors

41
api_tests/.eslintrc.json Normal file
View file

@ -0,0 +1,41 @@
{
"root": true,
"env": {
"browser": true
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"warnOnUnsupportedTypeScriptVersion": false
},
"rules": {
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,
"eqeqeq": 0,
"func-style": 0,
"import/no-duplicates": 0,
"max-statements": 0,
"max-params": 0,
"new-cap": 0,
"no-console": 0,
"no-duplicate-imports": 0,
"no-extra-parens": 0,
"no-return-assign": 0,
"no-throw-literal": 0,
"no-trailing-spaces": 0,
"no-unused-expressions": 0,
"no-useless-constructor": 0,
"no-useless-escape": 0,
"no-var": 0,
"prefer-const": 0,
"prefer-rest-params": 0,
"quote-props": 0,
"unicorn/filename-case": 0
}
}

View file

@ -1 +0,0 @@
package-manager-strict=false

View file

@ -1,56 +0,0 @@
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parser: tseslint.parser,
},
},
// For some reason this has to be in its own block
{
ignores: [
"putTypesInIndex.js",
"dist/*",
"docs/*",
".yalc",
"jest.config.js",
],
},
{
files: ["src/**/*"],
rules: {
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-var-requires": 0,
"arrow-body-style": 0,
curly: 0,
"eol-last": 0,
eqeqeq: 0,
"func-style": 0,
"import/no-duplicates": 0,
"max-statements": 0,
"max-params": 0,
"new-cap": 0,
"no-console": 0,
"no-duplicate-imports": 0,
"no-extra-parens": 0,
"no-return-assign": 0,
"no-throw-literal": 0,
"no-trailing-spaces": 0,
"no-unused-expressions": 0,
"no-useless-constructor": 0,
"no-useless-escape": 0,
"no-var": 0,
"prefer-const": 0,
"prefer-rest-params": 0,
"quote-props": 0,
"unicorn/filename-case": 0,
},
},
];

View file

@ -6,11 +6,10 @@
"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.4.0",
"scripts": { "scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && 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 post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts && jest -i image.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",
@ -20,18 +19,17 @@
"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.10",
"@types/node": "^20.12.4", "@types/node": "^20.9.4",
"@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^7.5.0", "@typescript-eslint/parser": "^6.12.0",
"download-file-sync": "^1.0.4", "download-file-sync": "^1.0.4",
"eslint": "^9.0.0", "eslint": "^8.54.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.5-alpha.1", "lemmy-js-client": "0.19.0-alpha.18",
"prettier": "^3.2.5", "prettier": "^3.1.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.4.4", "typescript": "^5.3.2"
"typescript-eslint": "^7.13.0"
} }
} }

BIN
api_tests/pict-rs Executable file

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -3,19 +3,14 @@
# it is expected that this script is called by run-federation-test.sh script. # it is expected that this script is called by run-federation-test.sh script.
set -e set -e
if [ -z "$LEMMY_LOG_LEVEL" ];
then
LEMMY_LOG_LEVEL=info
fi
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
# pictrs setup # pictrs setup
if [ ! -f "api_tests/pict-rs" ]; then if ! [ -f "pict-rs" ]; then
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
chmod +x api_tests/pict-rs chmod +x api_tests/pict-rs
fi fi
./api_tests/pict-rs \ ./api_tests/pict-rs \
@ -51,35 +46,32 @@ fi
echo "$PWD" echo "$PWD"
LOG_DIR=target/log
mkdir -p $LOG_DIR
echo "start alpha" echo "start alpha"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
target/lemmy_server >$LOG_DIR/lemmy_alpha.out 2>&1 & target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
echo "start beta" echo "start beta"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
target/lemmy_server >$LOG_DIR/lemmy_beta.out 2>&1 & target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
echo "start gamma" echo "start gamma"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 & target/lemmy_server >/tmp/lemmy_gamma.out 2>&1 &
echo "start delta" echo "start delta"
# An instance with only an allowlist for beta # An instance with only an allowlist for beta
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 & target/lemmy_server >/tmp/lemmy_delta.out 2>&1 &
echo "start epsilon" echo "start epsilon"
# An instance who has a blocklist, with lemmy-alpha blocked # An instance who has a blocklist, with lemmy-alpha blocked
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \ LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 & target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
echo "wait for all instances to start" echo "wait for all instances to start"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done

View file

@ -10,8 +10,8 @@ killall -s1 lemmy_server || true
./api_tests/prepare-drone-federation-test.sh ./api_tests/prepare-drone-federation-test.sh
popd popd
pnpm i yarn
pnpm api-test || true yarn api-test || true
killall -s1 lemmy_server || true killall -s1 lemmy_server || true
killall -s1 pict-rs || true killall -s1 pict-rs || true

View file

@ -37,15 +37,16 @@ import {
followCommunity, followCommunity,
blockCommunity, blockCommunity,
delay, delay,
saveUserSettings,
} from "./shared"; } from "./shared";
import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; import { CommentView, CommunityView } from "lemmy-js-client";
import { LemmyHttp } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined; let betaCommunity: CommunityView | undefined;
let postOnAlphaRes: PostResponse; let postOnAlphaRes: PostResponse;
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
await unfollows();
await Promise.all([followBeta(alpha), followBeta(gamma)]); await Promise.all([followBeta(alpha), followBeta(gamma)]);
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
if (betaCommunity) { if (betaCommunity) {
@ -53,7 +54,9 @@ beforeAll(async () => {
} }
}); });
afterAll(unfollows); afterAll(() => {
unfollows();
});
function assertCommentFederation( function assertCommentFederation(
commentOne?: CommentView, commentOne?: CommentView,
@ -126,9 +129,8 @@ test("Update a comment", async () => {
}); });
test("Delete a comment", async () => { test("Delete a comment", async () => {
let post = await createPost(alpha, betaCommunity!.community.id);
// creating a comment on alpha (remote from home of community) // creating a comment on alpha (remote from home of community)
let commentRes = await createComment(alpha, post.post_view.post.id); let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
// Find the comment on beta (home of community) // Find the comment on beta (home of community)
let betaComment = ( let betaComment = (
@ -156,7 +158,6 @@ test("Delete a comment", async () => {
commentRes.comment_view.comment.id, commentRes.comment_view.comment.id,
); );
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta // Make sure that comment is undefined on beta
await waitUntil( await waitUntil(
@ -255,16 +256,6 @@ test("Remove a comment from admin and community on different instance", async ()
betaComment.comment.id, betaComment.comment.id,
); );
expect(removeCommentRes.comment_view.comment.removed).toBe(true); expect(removeCommentRes.comment_view.comment.removed).toBe(true);
expect(removeCommentRes.comment_view.comment.content).toBe("");
// Comment text is also hidden from list
let listComments = await getComments(
beta,
removeCommentRes.comment_view.post.id,
);
expect(listComments.comments.length).toBe(1);
expect(listComments.comments[0].comment.removed).toBe(true);
expect(listComments.comments[0].comment.content).toBe("");
// Make sure its not removed on alpha // Make sure its not removed on alpha
let refetchedPostComments = await getComments( let refetchedPostComments = await getComments(
@ -354,26 +345,17 @@ test("Federated comment like", async () => {
test("Reply to a comment from another instance, get notification", async () => { test("Reply to a comment from another instance, get notification", async () => {
await alpha.markAllAsRead(); await alpha.markAllAsRead();
let betaCommunity = ( let betaCommunity = (await resolveBetaCommunity(alpha)).community;
await waitUntil(
() => resolveBetaCommunity(alpha),
c => !!c.community?.community.instance_id,
)
).community;
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id); const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
// Create a root-level trunk-branch comment on alpha // Create a root-level trunk-branch comment on alpha
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id); let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
// find that comment id on beta // find that comment id on beta
let betaComment = ( let betaComment = (
await waitUntil( await resolveComment(beta, commentRes.comment_view.comment)
() => resolveComment(beta, commentRes.comment_view.comment),
c => c.comment?.counts.score === 1,
)
).comment; ).comment;
if (!betaComment) { if (!betaComment) {
@ -424,10 +406,7 @@ test("Reply to a comment from another instance, get notification", async () => {
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1); expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
// check inbox of replies on alpha, fetching read/unread both // check inbox of replies on alpha, fetching read/unread both
let alphaRepliesRes = await waitUntil( let alphaRepliesRes = await getReplies(alpha);
() => getReplies(alpha),
r => r.replies.length > 0,
);
const alphaReply = alphaRepliesRes.replies.find( const alphaReply = alphaRepliesRes.replies.find(
r => r.comment.id === alphaComment.comment.id, r => r.comment.id === alphaComment.comment.id,
); );
@ -444,59 +423,6 @@ test("Reply to a comment from another instance, get notification", async () => {
assertCommentFederation(alphaReply, replyRes.comment_view); assertCommentFederation(alphaReply, replyRes.comment_view);
}); });
test("Bot reply notifications are filtered when bots are hidden", async () => {
const newAlphaBot = await registerUser(alpha, alphaUrl);
let form: SaveUserSettings = {
bot_account: true,
};
await saveUserSettings(newAlphaBot, form);
const alphaCommunity = (
await resolveCommunity(alpha, "!main@lemmy-alpha:8541")
).community;
if (!alphaCommunity) {
throw "Missing alpha community";
}
await alpha.markAllAsRead();
form = {
show_bot_accounts: false,
};
await saveUserSettings(alpha, form);
const postOnAlphaRes = await createPost(alpha, alphaCommunity.community.id);
// Bot reply to alpha's post
let commentRes = await createComment(
newAlphaBot,
postOnAlphaRes.post_view.post.id,
);
expect(commentRes).toBeDefined();
let alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(0);
let alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(0);
// This both restores the original state that may be expected by other tests
// implicitly and is used by the next steps to ensure replies are still
// returned when a user later decides to show bot accounts again.
form = {
show_bot_accounts: true,
};
await saveUserSettings(alpha, form);
alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(1);
alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(1);
expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe(
commentRes.comment_view.comment.id,
);
});
test("Mention beta from alpha", async () => { test("Mention beta from alpha", async () => {
if (!betaCommunity) throw Error("no community"); if (!betaCommunity) throw Error("no community");
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id); const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);

View file

@ -29,14 +29,12 @@ import {
delta, delta,
betaAllowedInstances, betaAllowedInstances,
searchPostLocal, searchPostLocal,
resolveBetaCommunity,
longDelay, longDelay,
editCommunity,
unfollows,
} from "./shared"; } from "./shared";
import { EditCommunity, EditSite } from "lemmy-js-client"; import { EditSite, LemmyHttp } from "lemmy-js-client";
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(unfollows);
function assertCommunityFederation( function assertCommunityFederation(
communityOne?: CommunityView, communityOne?: CommunityView,
@ -242,7 +240,7 @@ test("Admin actions in remote community are not federated to origin", async () =
); );
expect(banRes.banned).toBe(true); expect(banRes.banned).toBe(true);
// ban doesn't federate to community's origin instance alpha // ban doesnt federate to community's origin instance alpha
let alphaPost = (await resolvePost(alpha, gammaPost.post)).post; let alphaPost = (await resolvePost(alpha, gammaPost.post)).post;
expect(alphaPost?.creator_banned_from_community).toBe(false); expect(alphaPost?.creator_banned_from_community).toBe(false);
@ -379,9 +377,7 @@ test("User blocks instance, communities are hidden", async () => {
test("Community follower count is federated", async () => { test("Community follower count is federated", async () => {
// Follow the beta community from alpha // Follow the beta community from alpha
let community = await createCommunity(beta); let resolved = await resolveBetaCommunity(alpha);
let communityActorId = community.community_view.community.actor_id;
let resolved = await resolveCommunity(alpha, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -389,7 +385,7 @@ test("Community follower count is federated", async () => {
await followCommunity(alpha, true, resolved.community.community.id); await followCommunity(alpha, true, resolved.community.community.id);
let followed = ( let followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(alpha, communityActorId), () => resolveBetaCommunity(alpha),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -398,7 +394,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts.subscribers).toBe(1); expect(followed?.counts.subscribers).toBe(1);
// Follow the community from gamma // Follow the community from gamma
resolved = await resolveCommunity(gamma, communityActorId); resolved = await resolveBetaCommunity(gamma);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -406,7 +402,7 @@ test("Community follower count is federated", async () => {
await followCommunity(gamma, true, resolved.community.community.id); await followCommunity(gamma, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(gamma, communityActorId), () => resolveBetaCommunity(gamma),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -415,7 +411,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts?.subscribers).toBe(2); expect(followed?.counts?.subscribers).toBe(2);
// Follow the community from delta // Follow the community from delta
resolved = await resolveCommunity(delta, communityActorId); resolved = await resolveBetaCommunity(delta);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -423,7 +419,7 @@ test("Community follower count is federated", async () => {
await followCommunity(delta, true, resolved.community.community.id); await followCommunity(delta, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(delta, communityActorId), () => resolveBetaCommunity(delta),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -452,7 +448,7 @@ test("Dont receive community activities after unsubscribe", async () => {
); );
expect(communityRes1.community_view.counts.subscribers).toBe(2); expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesn't know about unfollow // temporarily block alpha, so that it doesnt know about unfollow
let editSiteForm: EditSite = {}; let editSiteForm: EditSite = {};
editSiteForm.allowed_instances = ["lemmy-epsilon"]; editSiteForm.allowed_instances = ["lemmy-epsilon"];
await beta.editSite(editSiteForm); await beta.editSite(editSiteForm);
@ -484,52 +480,3 @@ test("Dont receive community activities after unsubscribe", async () => {
let postResBeta = searchPostLocal(beta, postRes.post_view.post); let postResBeta = searchPostLocal(beta, postRes.post_view.post);
expect((await postResBeta).posts.length).toBe(0); expect((await postResBeta).posts.length).toBe(0);
}); });
test("Fetch community, includes posts", async () => {
let communityRes = await createCommunity(alpha);
expect(communityRes.community_view.community.name).toBeDefined();
expect(communityRes.community_view.counts.subscribers).toBe(1);
let postRes = await createPost(
alpha,
communityRes.community_view.community.id,
);
expect(postRes.post_view.post).toBeDefined();
let resolvedCommunity = await waitUntil(
() =>
resolveCommunity(beta, communityRes.community_view.community.actor_id),
c => c.community?.community.id != undefined,
);
let betaCommunity = resolvedCommunity.community;
expect(betaCommunity?.community.actor_id).toBe(
communityRes.community_view.community.actor_id,
);
await longDelay();
let post_listing = await getPosts(beta, "All", betaCommunity?.community.id);
expect(post_listing.posts.length).toBe(1);
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
});
test("Content in local-only community doesn't federate", async () => {
// create a community and set it local-only
let communityRes = (await createCommunity(alpha)).community_view.community;
let form: EditCommunity = {
community_id: communityRes.id,
visibility: "LocalOnly",
};
await editCommunity(alpha, form);
// cant resolve the community from another instance
await expect(
resolveCommunity(beta, communityRes.actor_id),
).rejects.toStrictEqual(Error("couldnt_find_object"));
// create a post, also cant resolve it
let postRes = await createPost(alpha, communityRes.id);
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
Error("couldnt_find_object"),
);
});

View file

@ -5,49 +5,40 @@ import {
setupLogins, setupLogins,
resolveBetaCommunity, resolveBetaCommunity,
followCommunity, followCommunity,
unfollowRemotes,
getSite, getSite,
waitUntil, waitUntil,
beta, beta,
betaUrl, betaUrl,
registerUser, registerUser,
unfollows,
} from "./shared"; } from "./shared";
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(unfollows); afterAll(() => {
unfollowRemotes(alpha);
});
test("Follow local community", async () => { test("Follow local community", async () => {
let user = await registerUser(beta, betaUrl); let user = await registerUser(beta, betaUrl);
let community = (await resolveBetaCommunity(user)).community!; let community = (await resolveBetaCommunity(user)).community!;
expect(community.counts.subscribers).toBe(1); expect(community.counts.subscribers).toBe(1);
expect(community.counts.subscribers_local).toBe(1);
let follow = await followCommunity(user, true, community.community.id); let follow = await followCommunity(user, true, community.community.id);
// Make sure the follow response went through // Make sure the follow response went through
expect(follow.community_view.community.local).toBe(true); expect(follow.community_view.community.local).toBe(true);
expect(follow.community_view.subscribed).toBe("Subscribed"); expect(follow.community_view.subscribed).toBe("Subscribed");
expect(follow.community_view.counts.subscribers).toBe(2); expect(follow.community_view.counts.subscribers).toBe(2);
expect(follow.community_view.counts.subscribers_local).toBe(2);
// Test an unfollow // Test an unfollow
let unfollow = await followCommunity(user, false, community.community.id); let unfollow = await followCommunity(user, false, community.community.id);
expect(unfollow.community_view.subscribed).toBe("NotSubscribed"); expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
expect(unfollow.community_view.counts.subscribers).toBe(1); expect(unfollow.community_view.counts.subscribers).toBe(1);
expect(unfollow.community_view.counts.subscribers_local).toBe(1);
}); });
test("Follow federated community", async () => { test("Follow federated community", async () => {
// It takes about 1 second for the community aggregates to federate let betaCommunity = (await resolveBetaCommunity(alpha)).community;
let betaCommunity = (
await waitUntil(
() => resolveBetaCommunity(alpha),
c =>
c.community?.counts.subscribers === 1 &&
c.community.counts.subscribers_local === 0,
)
).community;
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -64,12 +55,10 @@ test("Follow federated community", async () => {
expect(betaCommunity?.community.local).toBe(false); expect(betaCommunity?.community.local).toBe(false);
expect(betaCommunity?.community.name).toBe("main"); expect(betaCommunity?.community.name).toBe("main");
expect(betaCommunity?.subscribed).toBe("Subscribed"); expect(betaCommunity?.subscribed).toBe("Subscribed");
expect(betaCommunity?.counts.subscribers_local).toBe(1);
// check that unfollow was federated // check that unfollow was federated
let communityOnBeta1 = await resolveBetaCommunity(beta); let communityOnBeta1 = await resolveBetaCommunity(beta);
expect(communityOnBeta1.community?.counts.subscribers).toBe(2); expect(communityOnBeta1.community?.counts.subscribers).toBe(2);
expect(communityOnBeta1.community?.counts.subscribers_local).toBe(1);
// Check it from local // Check it from local
let site = await getSite(alpha); let site = await getSite(alpha);
@ -94,5 +83,4 @@ test("Follow federated community", async () => {
// check that unfollow was federated // check that unfollow was federated
let communityOnBeta2 = await resolveBetaCommunity(beta); let communityOnBeta2 = await resolveBetaCommunity(beta);
expect(communityOnBeta2.community?.counts.subscribers).toBe(1); expect(communityOnBeta2.community?.counts.subscribers).toBe(1);
expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1);
}); });

View file

@ -8,49 +8,32 @@ import {
} from "lemmy-js-client"; } from "lemmy-js-client";
import { import {
alpha, alpha,
alphaImage,
alphaUrl, alphaUrl,
beta, beta,
betaUrl, betaUrl,
createCommunity,
createPost, createPost,
deleteAllImages,
epsilon,
followCommunity,
gamma,
getSite, getSite,
imageFetchLimit,
registerUser, registerUser,
resolveBetaCommunity, resolveBetaCommunity,
resolveCommunity,
resolvePost,
setupLogins, setupLogins,
waitForPost, unfollowRemotes,
unfollows,
getPost,
waitUntil,
createPostWithThumbnail,
sampleImage,
sampleSite,
} from "./shared"; } from "./shared";
import fs = require("fs");
const downloadFileSync = require("download-file-sync"); const downloadFileSync = require("download-file-sync");
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(async () => { afterAll(() => {
await Promise.all([unfollows(), deleteAllImages(alpha)]); unfollowRemotes(alpha);
}); });
test("Upload image and delete it", async () => { test("Upload image and delete it", async () => {
// Before running this test, you need to delete all previous images in the DB // upload test image
await deleteAllImages(alpha); const upload_image = fs.readFileSync("test.png");
// Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
// in testing mode.
const upload_form: UploadImage = { const upload_form: UploadImage = {
image: Buffer.from("test"), image: upload_image,
}; };
const upload = await alphaImage.uploadImage(upload_form); const upload = await alpha.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined(); expect(upload.files![0].file).toBeDefined();
expect(upload.files![0].delete_token).toBeDefined(); expect(upload.files![0].delete_token).toBeDefined();
expect(upload.url).toBeDefined(); expect(upload.url).toBeDefined();
@ -60,58 +43,26 @@ test("Upload image and delete it", async () => {
const content = downloadFileSync(upload.url); const content = downloadFileSync(upload.url);
expect(content.length).toBeGreaterThan(0); expect(content.length).toBeGreaterThan(0);
// Ensure that it comes back with the list_media endpoint
const listMediaRes = await alphaImage.listMedia();
expect(listMediaRes.images.length).toBe(1);
// Ensure that it also comes back with the admin all images
const listAllMediaRes = await alphaImage.listAllMedia({
limit: imageFetchLimit,
});
// This number comes from all the previous thumbnails fetched in other tests.
const previousThumbnails = 1;
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
// The deleteUrl is a combination of the endpoint, delete token, and alias
let firstImage = listMediaRes.images[0];
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
expect(deleteUrl).toBe(upload.delete_url);
// Make sure the uploader is correct
expect(firstImage.person.actor_id).toBe(
`http://lemmy-alpha:8541/u/lemmy_alpha`,
);
// delete image // delete image
const delete_form: DeleteImage = { const delete_form: DeleteImage = {
token: upload.files![0].delete_token, token: upload.files![0].delete_token,
filename: upload.files![0].file, filename: upload.files![0].file,
}; };
const delete_ = await alphaImage.deleteImage(delete_form); const delete_ = await alpha.deleteImage(delete_form);
expect(delete_).toBe(true); expect(delete_).toBe(true);
// ensure that image is deleted // ensure that image is deleted
const content2 = downloadFileSync(upload.url); const content2 = downloadFileSync(upload.url);
expect(content2).toBe(""); expect(content2).toBe("");
// Ensure that it shows the image is deleted
const deletedListMediaRes = await alphaImage.listMedia();
expect(deletedListMediaRes.images.length).toBe(0);
// Ensure that the admin shows its deleted
const deletedListAllMediaRes = await alphaImage.listAllMedia({
limit: imageFetchLimit,
});
expect(deletedListAllMediaRes.images.length).toBe(previousThumbnails - 1);
}); });
test("Purge user, uploaded image removed", async () => { test("Purge user, uploaded image removed", async () => {
let user = await registerUser(alphaImage, alphaUrl); let user = await registerUser(alpha, alphaUrl);
// upload test image // upload test image
const upload_image = fs.readFileSync("test.png");
const upload_form: UploadImage = { const upload_form: UploadImage = {
image: Buffer.from("test"), image: upload_image,
}; };
const upload = await user.uploadImage(upload_form); const upload = await user.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined(); expect(upload.files![0].file).toBeDefined();
@ -125,10 +76,10 @@ test("Purge user, uploaded image removed", async () => {
// purge user // purge user
let site = await getSite(user); let site = await getSite(user);
const purgeForm: PurgePerson = { const purge_form: PurgePerson = {
person_id: site.my_user!.local_user_view.person.id, person_id: site.my_user!.local_user_view.person.id,
}; };
const delete_ = await alphaImage.purgePerson(purgeForm); const delete_ = await alpha.purgePerson(purge_form);
expect(delete_.success).toBe(true); expect(delete_.success).toBe(true);
// ensure that image is deleted // ensure that image is deleted
@ -140,8 +91,9 @@ test("Purge post, linked image removed", async () => {
let user = await registerUser(beta, betaUrl); let user = await registerUser(beta, betaUrl);
// upload test image // upload test image
const upload_image = fs.readFileSync("test.png");
const upload_form: UploadImage = { const upload_form: UploadImage = {
image: Buffer.from("test"), image: upload_image,
}; };
const upload = await user.uploadImage(upload_form); const upload = await user.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined(); expect(upload.files![0].file).toBeDefined();
@ -160,208 +112,15 @@ test("Purge post, linked image removed", async () => {
upload.url, upload.url,
); );
expect(post.post_view.post.url).toBe(upload.url); expect(post.post_view.post.url).toBe(upload.url);
expect(post.post_view.image_details).toBeDefined();
// purge post // purge post
const purgeForm: PurgePost = { const purge_form: PurgePost = {
post_id: post.post_view.post.id, post_id: post.post_view.post.id,
}; };
const delete_ = await beta.purgePost(purgeForm); const delete_ = await beta.purgePost(purge_form);
expect(delete_.success).toBe(true); expect(delete_.success).toBe(true);
// ensure that image is deleted // ensure that image is deleted
const content2 = downloadFileSync(upload.url); const content2 = downloadFileSync(upload.url);
expect(content2).toBe(""); expect(content2).toBe("");
}); });
test("Images in remote image post are proxied if setting enabled", async () => {
let community = await createCommunity(gamma);
let postRes = await createPost(
gamma,
community.community_view.community.id,
sampleImage,
`![](${sampleImage})`,
);
const post = postRes.post_view.post;
expect(post).toBeDefined();
// Make sure it fetched the image details
expect(postRes.post_view.image_details).toBeDefined();
// remote image gets proxied after upload
expect(
post.thumbnail_url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy();
expect(
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
).toBeTruthy();
// Make sure that it ends with jpg, to be sure its an image
expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
expect(epsilonPostRes.post).toBeDefined();
// Fetch the post again, the metadata should be backgrounded now
// Wait for the metadata to get fetched, since this is backgrounded now
let epsilonPostRes2 = await waitUntil(
() => getPost(epsilon, epsilonPostRes.post!.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
const epsilonPost = epsilonPostRes2.post_view.post;
expect(
epsilonPost.thumbnail_url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
expect(
epsilonPost.body?.startsWith(
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
// Make sure that it ends with jpg, to be sure its an image
expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
});
test("Thumbnail of remote image link is proxied if setting enabled", async () => {
let community = await createCommunity(gamma);
let postRes = await createPost(
gamma,
community.community_view.community.id,
// The sample site metadata thumbnail ends in png
sampleSite,
);
const post = postRes.post_view.post;
expect(post).toBeDefined();
// remote image gets proxied after upload
expect(
post.thumbnail_url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy();
// Make sure that it ends with png, to be sure its an image
expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy();
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
expect(epsilonPostRes.post).toBeDefined();
let epsilonPostRes2 = await waitUntil(
() => getPost(epsilon, epsilonPostRes.post!.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
const epsilonPost = epsilonPostRes2.post_view.post;
expect(
epsilonPost.thumbnail_url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
// Make sure that it ends with png, to be sure its an image
expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy();
});
test("No image proxying if setting is disabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(alpha);
let betaCommunity = await resolveCommunity(
beta,
community.community_view.community.actor_id,
);
await followCommunity(beta, true, betaCommunity.community!.community.id);
const upload_form: UploadImage = {
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
let post = await createPost(
alpha,
community.community_view.community.id,
upload.url,
`![](${sampleImage})`,
);
expect(post.post_view.post).toBeDefined();
// remote image doesn't get proxied after upload
expect(
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy();
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
let betaPost = await waitForPost(
beta,
post.post_view.post,
res => res?.post.alt_text != null,
);
expect(betaPost.post).toBeDefined();
// remote image doesn't get proxied after federation
expect(
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy();
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
// Make sure the alt text got federated
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
});
test("Make regular post, and give it a custom thumbnail", async () => {
const uploadForm1: UploadImage = {
image: Buffer.from("testRegular1"),
};
const upload1 = await alphaImage.uploadImage(uploadForm1);
const community = await createCommunity(alphaImage);
// Use wikipedia since it has an opengraph image
const wikipediaUrl = "https://wikipedia.org/";
let post = await createPostWithThumbnail(
alphaImage,
community.community_view.community.id,
wikipediaUrl,
upload1.url!,
);
// Wait for the metadata to get fetched, since this is backgrounded now
post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
expect(post.post_view.post.url).toBe(wikipediaUrl);
// Make sure it uses custom thumbnail
expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
});
test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
const uploadForm1: UploadImage = {
image: Buffer.from("test1"),
};
const upload1 = await alphaImage.uploadImage(uploadForm1);
const uploadForm2: UploadImage = {
image: Buffer.from("test2"),
};
const upload2 = await alphaImage.uploadImage(uploadForm2);
const community = await createCommunity(alphaImage);
let post = await createPostWithThumbnail(
alphaImage,
community.community_view.community.id,
upload1.url!,
upload2.url!,
);
post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
expect(post.post_view.post.url).toBe(upload1.url);
// Make sure the custom thumbnail is ignored
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
});

View file

@ -18,12 +18,12 @@ import {
resolveBetaCommunity, resolveBetaCommunity,
createComment, createComment,
deletePost, deletePost,
delay,
removePost, removePost,
getPost, getPost,
unfollowRemotes, unfollowRemotes,
resolvePerson, resolvePerson,
banPersonFromSite, banPersonFromSite,
searchPostLocal,
followCommunity, followCommunity,
banPersonFromCommunity, banPersonFromCommunity,
reportPost, reportPost,
@ -37,10 +37,9 @@ import {
waitForPost, waitForPost,
alphaUrl, alphaUrl,
loginUser, loginUser,
createCommunity,
} from "./shared"; } from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView"; import { PostView } from "lemmy-js-client/dist/types/PostView";
import { EditSite, ResolveObject } from "lemmy-js-client"; import { LemmyHttp, ResolveObject } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined; let betaCommunity: CommunityView | undefined;
@ -48,22 +47,14 @@ beforeAll(async () => {
await setupLogins(); await setupLogins();
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
expect(betaCommunity).toBeDefined(); expect(betaCommunity).toBeDefined();
await unfollows();
}); });
afterAll(unfollows); afterAll(() => {
unfollows();
async function assertPostFederation(postOne: PostView, postTwo: PostView) { });
// Link metadata is generated in background task and may not be ready yet at this time,
// so wait for it explicitly. For removed posts we cant refetch anything.
postOne = await waitForPost(beta, postOne.post, res => {
return res === null || res?.post.embed_title !== null;
});
postTwo = await waitForPost(
beta,
postTwo.post,
res => res === null || res?.post.embed_title !== null,
);
function assertPostFederation(postOne?: PostView, postTwo?: PostView) {
expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id); expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id);
expect(postOne?.post.name).toBe(postTwo?.post.name); expect(postOne?.post.name).toBe(postTwo?.post.name);
expect(postOne?.post.body).toBe(postTwo?.post.body); expect(postOne?.post.body).toBe(postTwo?.post.body);
@ -81,23 +72,11 @@ async function assertPostFederation(postOne: PostView, postTwo: PostView) {
} }
test("Create a post", async () => { test("Create a post", async () => {
// Setup some allowlists and blocklists
const editSiteForm: EditSite = {};
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"];
await epsilon.editSite(editSiteForm);
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
let postRes = await createPost( let postRes = await createPost(alpha, betaCommunity.community.id);
alpha,
betaCommunity.community.id,
"https://example.com/",
"აშშ ითხოვს ირანს დაუყოვნებლივ გაანთავისუფლოს დაკავებული ნავთობის ტანკერი",
);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false); expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true); expect(postRes.post_view.creator.local).toBe(true);
@ -114,7 +93,7 @@ test("Create a post", async () => {
expect(betaPost?.community.local).toBe(true); expect(betaPost?.community.local).toBe(true);
expect(betaPost?.creator.local).toBe(false); expect(betaPost?.creator.local).toBe(false);
expect(betaPost?.counts.score).toBe(1); expect(betaPost?.counts.score).toBe(1);
await assertPostFederation(betaPost, postRes.post_view); assertPostFederation(betaPost, postRes.post_view);
// 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(
@ -125,12 +104,6 @@ test("Create a post", async () => {
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("couldnt_find_object"));
// remove added allow/blocklists
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = [];
await delta.editSite(editSiteForm);
await epsilon.editSite(editSiteForm);
}); });
test("Create a post in a non-existent community", async () => { test("Create a post in a non-existent community", async () => {
@ -162,7 +135,7 @@ test("Unlike a post", async () => {
expect(betaPost?.community.local).toBe(true); expect(betaPost?.community.local).toBe(true);
expect(betaPost?.creator.local).toBe(false); expect(betaPost?.creator.local).toBe(false);
expect(betaPost?.counts.score).toBe(0); expect(betaPost?.counts.score).toBe(0);
await assertPostFederation(betaPost, postRes.post_view); assertPostFederation(betaPost, postRes.post_view);
}); });
test("Update a post", async () => { test("Update a post", async () => {
@ -183,7 +156,7 @@ test("Update a post", async () => {
expect(betaPost.community.local).toBe(true); expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.name).toBe(updatedName); expect(betaPost.post.name).toBe(updatedName);
await assertPostFederation(betaPost, updatedPost.post_view); assertPostFederation(betaPost, updatedPost.post_view);
// Make sure lemmy beta cannot update the post // Make sure lemmy beta cannot update the post
await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual( await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual(
@ -225,35 +198,12 @@ test("Sticky a post", async () => {
if (!gammaPost) { if (!gammaPost) {
throw "Missing gamma post"; throw "Missing gamma post";
} }
// This has been failing occasionally let gammaTrySticky = await featurePost(gamma, true, gammaPost.post);
await featurePost(gamma, true, gammaPost.post);
let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post;
// expect(gammaTrySticky.post_view.post.featured_community).toBe(true); expect(gammaTrySticky.post_view.post.featured_community).toBe(true);
expect(betaPost3?.post.featured_community).toBe(false); expect(betaPost3?.post.featured_community).toBe(false);
}); });
test("Collection of featured posts gets federated", async () => {
// create a new community and feature a post
let community = await createCommunity(alpha);
let post = await createPost(alpha, community.community_view.community.id);
let featuredPost = await featurePost(alpha, true, post.post_view.post);
expect(featuredPost.post_view.post.featured_community).toBe(true);
// fetch the community, ensure that post is also fetched and marked as featured
let betaCommunity = await resolveCommunity(
beta,
community.community_view.community.actor_id,
);
expect(betaCommunity).toBeDefined();
const betaPost = await waitForPost(
beta,
post.post_view.post,
post => post?.post.featured_community === true,
);
expect(betaPost).toBeDefined();
});
test("Lock a post", async () => { test("Lock a post", async () => {
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
@ -277,10 +227,8 @@ test("Lock a post", async () => {
post => !!post && post.post.locked, post => !!post && post.post.locked,
); );
// Try to make a new comment there, on alpha. For this we need to create a normal // Try to make a new comment there, on alpha
// user account because admins/mods can comment in locked posts. await expect(createComment(alpha, alphaPost1.post.id)).rejects.toStrictEqual(
let user = await registerUser(alpha, alphaUrl);
await expect(createComment(user, alphaPost1.post.id)).rejects.toStrictEqual(
Error("locked"), Error("locked"),
); );
@ -299,7 +247,7 @@ test("Lock a post", async () => {
expect(alphaPost2.post.locked).toBe(false); expect(alphaPost2.post.locked).toBe(false);
// Try to create a new comment, on alpha // Try to create a new comment, on alpha
let commentAlpha = await createComment(user, alphaPost1.post.id); let commentAlpha = await createComment(alpha, alphaPost1.post.id);
expect(commentAlpha).toBeDefined(); expect(commentAlpha).toBeDefined();
}); });
@ -334,7 +282,7 @@ test("Delete a post", async () => {
throw "Missing beta post 2"; throw "Missing beta post 2";
} }
expect(betaPost2.post.deleted).toBe(false); expect(betaPost2.post.deleted).toBe(false);
await assertPostFederation(betaPost2, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post_view);
// Make sure lemmy beta cannot delete the post // Make sure lemmy beta cannot delete the post
await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual( await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual(
@ -377,7 +325,7 @@ test("Remove a post from admin and community on different instance", async () =>
// Make sure lemmy beta sees post is undeleted // Make sure lemmy beta sees post is undeleted
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
expect(betaPost2?.post.removed).toBe(false); expect(betaPost2?.post.removed).toBe(false);
await assertPostFederation(betaPost2!, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post_view);
}); });
test("Remove a post from admin and community on same instance", async () => { test("Remove a post from admin and community on same instance", async () => {
@ -408,7 +356,7 @@ test("Remove a post from admin and community on same instance", async () => {
p => p?.post_view.post.removed ?? false, p => p?.post_view.post.removed ?? false,
); );
expect(alphaPost?.post_view.post.removed).toBe(true); expect(alphaPost?.post_view.post.removed).toBe(true);
await assertPostFederation(alphaPost.post_view, removePostRes.post_view); assertPostFederation(alphaPost.post_view, removePostRes.post_view);
// Undelete // Undelete
let undeletedPost = await removePost(beta, false, betaPost.post); let undeletedPost = await removePost(beta, false, betaPost.post);
@ -421,7 +369,7 @@ test("Remove a post from admin and community on same instance", async () => {
p => !!p && !p.post.removed, p => !!p && !p.post.removed,
); );
expect(alphaPost2.post.removed).toBe(false); expect(alphaPost2.post.removed).toBe(false);
await assertPostFederation(alphaPost2, undeletedPost.post_view); assertPostFederation(alphaPost2, undeletedPost.post_view);
await unfollowRemotes(alpha); await unfollowRemotes(alpha);
}); });
@ -437,34 +385,30 @@ test("Search for a post", async () => {
expect(betaPost?.post.name).toBeDefined(); expect(betaPost?.post.name).toBeDefined();
}); });
test("Enforce site ban federation for local user", async () => { test("Enforce site ban for federated user", async () => {
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
// create a test user // create a test user
let alphaUserHttp = await registerUser(alpha, alphaUrl); let alpha_user = await registerUser(alpha, alphaUrl);
let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view let alphaUserPerson = (await getSite(alpha_user)).my_user?.local_user_view
.person; .person;
let alphaUserActorId = alphaUserPerson?.actor_id; let alphaUserActorId = alphaUserPerson?.actor_id;
if (!alphaUserActorId) { if (!alphaUserActorId) {
throw "Missing alpha user actor id"; throw "Missing alpha user actor id";
} }
expect(alphaUserActorId).toBeDefined(); expect(alphaUserActorId).toBeDefined();
await followBeta(alphaUserHttp); let alphaPerson = (await resolvePerson(alpha_user, alphaUserActorId!)).person;
let alphaPerson = (await resolvePerson(alphaUserHttp, alphaUserActorId!))
.person;
if (!alphaPerson) { if (!alphaPerson) {
throw "Missing alpha person"; throw "Missing alpha person";
} }
expect(alphaPerson).toBeDefined(); expect(alphaPerson).toBeDefined();
// alpha makes post in beta community, it federates to beta instance // alpha makes post in beta community, it federates to beta instance
let postRes1 = await createPost(alphaUserHttp, betaCommunity.community.id); let postRes1 = await createPost(alpha_user, betaCommunity.community.id);
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post); let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
// ban alpha from its own instance // ban alpha from its instance
let banAlpha = await banPersonFromSite( let banAlpha = await banPersonFromSite(
alpha, alpha,
alphaPerson.person.id, alphaPerson.person.id,
@ -481,11 +425,10 @@ test("Enforce site ban federation for local user", async () => {
expect(alphaUserOnBeta1.person?.person.banned).toBe(true); expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
// existing alpha post should be removed on beta // existing alpha post should be removed on beta
let betaBanRes = await waitUntil( await waitUntil(
() => getPost(beta, searchBeta1.post.id), () => getPost(beta, searchBeta1.post.id),
s => s.post_view.post.removed, s => s.post_view.post.removed,
); );
expect(betaBanRes.post_view.post.removed).toBe(true);
// Unban alpha // Unban alpha
let unBanAlpha = await banPersonFromSite( let unBanAlpha = await banPersonFromSite(
@ -501,84 +444,21 @@ test("Enforce site ban federation for local user", async () => {
throw "Missing alpha person"; throw "Missing alpha person";
} }
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name); let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
alphaUserHttp.setHeaders({ alpha_user.setHeaders({
Authorization: "Bearer " + newAlphaUserJwt.jwt, Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "",
}); });
// alpha makes new post in beta community, it federates // alpha makes new post in beta community, it federates
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id); let postRes2 = await createPost(alpha_user, betaCommunity!.community.id);
await waitForPost(beta, postRes2.post_view.post); await waitForPost(beta, postRes2.post_view.post);
await unfollowRemotes(alpha);
});
test("Enforce site ban federation for federated user", async () => {
if (!betaCommunity) {
throw "Missing beta community";
}
// create a test user
let alphaUserHttp = await registerUser(alpha, alphaUrl);
let alphaUserPerson = (await getSite(alphaUserHttp)).my_user?.local_user_view
.person;
let alphaUserActorId = alphaUserPerson?.actor_id;
if (!alphaUserActorId) {
throw "Missing alpha user actor id";
}
expect(alphaUserActorId).toBeDefined();
await followBeta(alphaUserHttp);
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId!); let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId!);
expect(alphaUserOnBeta2.person?.person.banned).toBe(false); expect(alphaUserOnBeta2.person?.person.banned).toBe(false);
if (!alphaUserOnBeta2.person) {
throw "Missing alpha person";
}
// alpha makes post in beta community, it federates to beta instance
let postRes1 = await createPost(alphaUserHttp, betaCommunity.community.id);
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
expect(searchBeta1.post).toBeDefined();
// Now ban and remove their data from beta
let banAlphaOnBeta = await banPersonFromSite(
beta,
alphaUserOnBeta2.person.person.id,
true,
true,
);
expect(banAlphaOnBeta.banned).toBe(true);
// The beta site ban should NOT be federated to alpha
let alphaPerson2 = (await getSite(alphaUserHttp)).my_user!.local_user_view
.person;
expect(alphaPerson2.banned).toBe(false);
// existing alpha post should be removed on beta
let betaBanRes = await waitUntil(
() => getPost(beta, searchBeta1.post.id),
s => s.post_view.post.removed,
);
expect(betaBanRes.post_view.post.removed).toBe(true);
// existing alpha's post to the beta community should be removed on alpha
let alphaPostAfterRemoveOnBeta = await waitUntil(
() => getPost(alpha, postRes1.post_view.post.id),
s => s.post_view.post.removed,
);
expect(betaBanRes.post_view.post.removed).toBe(true);
expect(alphaPostAfterRemoveOnBeta.post_view.post.removed).toBe(true);
expect(
alphaPostAfterRemoveOnBeta.post_view.creator_banned_from_community,
).toBe(true);
await unfollowRemotes(alpha);
}); });
test("Enforce community ban for federated user", async () => { test.skip("Enforce community ban for federated user", async () => {
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
await followBeta(alpha);
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`; let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person; let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
if (!alphaPerson) { if (!alphaPerson) {
@ -588,46 +468,38 @@ test("Enforce community ban for federated user", async () => {
// make a post in beta, it goes through // make a post in beta, it goes through
let postRes1 = await createPost(alpha, betaCommunity.community.id); let postRes1 = await createPost(alpha, betaCommunity.community.id);
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post); let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
expect(searchBeta1.post).toBeDefined(); expect(searchBeta1.posts[0]).toBeDefined();
// ban alpha from beta community // ban alpha from beta community
let banAlpha = await banPersonFromCommunity( let banAlpha = await banPersonFromCommunity(
beta, beta,
alphaPerson.person.id, alphaPerson.person.id,
searchBeta1.community.id, 2,
true, true,
true, true,
); );
expect(banAlpha.banned).toBe(true); expect(banAlpha.banned).toBe(true);
// ensure that the post by alpha got removed // ensure that the post by alpha got removed
let removePostRes = await waitUntil( await expect(getPost(alpha, searchBeta1.posts[0].post.id)).rejects.toBe(
() => getPost(alpha, postRes1.post_view.post.id), Error("unknown"),
s => s.post_view.post.removed,
); );
expect(removePostRes.post_view.post.removed).toBe(true);
expect(removePostRes.post_view.creator_banned_from_community).toBe(true);
expect(removePostRes.community_view.banned_from_community).toBe(true);
// 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)).rejects.toBe(
createPost(alpha, betaCommunity.community.id), Error("banned_from_community"),
).rejects.toStrictEqual(Error("banned_from_community")); );
// Unban alpha // Unban alpha
let unBanAlpha = await banPersonFromCommunity( let unBanAlpha = await banPersonFromCommunity(
beta, beta,
alphaPerson.person.id, alphaPerson.person.id,
searchBeta1.community.id, 2,
false, false,
false, false,
); );
expect(unBanAlpha.banned).toBe(false); expect(unBanAlpha.banned).toBe(false);
// Need to re-follow the community
await followBeta(alpha);
let postRes3 = await createPost(alpha, betaCommunity.community.id); let postRes3 = await createPost(alpha, betaCommunity.community.id);
expect(postRes3.post_view.post).toBeDefined(); expect(postRes3.post_view.post).toBeDefined();
expect(postRes3.post_view.community.local).toBe(false); expect(postRes3.post_view.community.local).toBe(false);
@ -635,86 +507,57 @@ test("Enforce community ban for federated user", async () => {
expect(postRes3.post_view.counts.score).toBe(1); expect(postRes3.post_view.counts.score).toBe(1);
// Make sure that post makes it to beta community // Make sure that post makes it to beta community
let postRes4 = await waitForPost(beta, postRes3.post_view.post); let searchBeta2 = await searchPostLocal(beta, postRes3.post_view.post);
expect(postRes4.post).toBeDefined(); expect(searchBeta2.posts[0]).toBeDefined();
expect(postRes4.creator_banned_from_community).toBe(false);
await unfollowRemotes(alpha);
}); });
test("A and G subscribe to B (center) A posts, it gets announced to G", async () => { test("A and G subscribe to B (center) A posts, it gets announced to G", async () => {
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
} }
await followBeta(alpha);
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post; let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post;
expect(betaPost?.post.name).toBeDefined(); expect(betaPost?.post.name).toBeDefined();
await unfollowRemotes(alpha);
}); });
test("Report a post", async () => { test("Report a post", async () => {
// Create post from alpha // Note, this is a different one from the setup
let alphaCommunity = (await resolveBetaCommunity(alpha)).community!; let betaCommunity = (await resolveBetaCommunity(beta)).community;
await followBeta(alpha); if (!betaCommunity) {
let postRes = await createPost(alpha, alphaCommunity.community.id); throw "Missing beta community";
}
let postRes = await createPost(beta, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
if (!alphaPost) { if (!alphaPost) {
throw "Missing alpha post"; throw "Missing alpha post";
} }
let alphaReport = (
// Send report from gamma await reportPost(alpha, alphaPost.post.id, randomString(10))
let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!;
let gammaReport = (
await reportPost(gamma, gammaPost.post.id, randomString(10))
).post_report_view.post_report; ).post_report_view.post_report;
expect(gammaReport).toBeDefined();
// Report was federated to community instance
let betaReport = (await waitUntil( let betaReport = (await waitUntil(
() => () =>
listPostReports(beta).then(p => listPostReports(beta).then(p =>
p.post_reports.find( p.post_reports.find(
r => r =>
r.post_report.original_post_name === gammaReport.original_post_name, r.post_report.original_post_name === alphaReport.original_post_name,
), ),
), ),
res => !!res, res => !!res,
))!.post_report; ))!.post_report;
expect(betaReport).toBeDefined(); expect(betaReport).toBeDefined();
expect(betaReport.resolved).toBe(false); expect(betaReport.resolved).toBe(false);
expect(betaReport.original_post_name).toBe(gammaReport.original_post_name); expect(betaReport.original_post_name).toBe(alphaReport.original_post_name);
//expect(betaReport.original_post_url).toBe(gammaReport.original_post_url); expect(betaReport.original_post_url).toBe(alphaReport.original_post_url);
expect(betaReport.original_post_body).toBe(gammaReport.original_post_body); expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
expect(betaReport.reason).toBe(gammaReport.reason); expect(betaReport.reason).toBe(alphaReport.reason);
await unfollowRemotes(alpha);
// Report was federated to poster's instance
let alphaReport = (await waitUntil(
() =>
listPostReports(alpha).then(p =>
p.post_reports.find(
r =>
r.post_report.original_post_name === gammaReport.original_post_name,
),
),
res => !!res,
))!.post_report;
expect(alphaReport).toBeDefined();
expect(alphaReport.resolved).toBe(false);
expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name);
//expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url);
expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body);
expect(alphaReport.reason).toBe(gammaReport.reason);
}); });
test("Fetch post via redirect", async () => { test("Fetch post via redirect", async () => {
await followBeta(alpha);
let alphaPost = await createPost(alpha, betaCommunity!.community.id); let alphaPost = await createPost(alpha, betaCommunity!.community.id);
expect(alphaPost.post_view.post).toBeDefined(); expect(alphaPost.post_view.post).toBeDefined();
// Make sure that post is liked on beta // Make sure that post is liked on beta
@ -735,47 +578,4 @@ test("Fetch post via redirect", async () => {
let gammaPost = await gamma.resolveObject(form); let gammaPost = await gamma.resolveObject(form);
expect(gammaPost).toBeDefined(); expect(gammaPost).toBeDefined();
expect(gammaPost.post?.post.ap_id).toBe(alphaPost.post_view.post.ap_id); expect(gammaPost.post?.post.ap_id).toBe(alphaPost.post_view.post.ap_id);
await unfollowRemotes(alpha);
});
test("Block post that contains banned URL", async () => {
let editSiteForm: EditSite = {
blocked_urls: ["https://evil.com/"],
};
await epsilon.editSite(editSiteForm);
await delay();
if (!betaCommunity) {
throw "Missing beta community";
}
expect(
createPost(epsilon, betaCommunity.community.id, "https://evil.com"),
).rejects.toStrictEqual(Error("blocked_url"));
// Later tests need this to be empty
editSiteForm.blocked_urls = [];
await epsilon.editSite(editSiteForm);
});
test("Fetch post with redirect", async () => {
let alphaPost = await createPost(alpha, betaCommunity!.community.id);
expect(alphaPost.post_view.post).toBeDefined();
// beta fetches from alpha as usual
let betaPost = await resolvePost(beta, alphaPost.post_view.post);
expect(betaPost.post).toBeDefined();
// gamma fetches from beta, and gets redirected to alpha
let gammaPost = await resolvePost(gamma, betaPost.post!.post);
expect(gammaPost.post).toBeDefined();
// fetch remote object from local url, which redirects to the original url
let form: ResolveObject = {
q: `http://lemmy-gamma:8561/post/${gammaPost.post!.post.id}`,
};
let gammaPost2 = await gamma.resolveObject(form);
expect(gammaPost2.post).toBeDefined();
}); });

View file

@ -8,9 +8,8 @@ import {
editPrivateMessage, editPrivateMessage,
listPrivateMessages, listPrivateMessages,
deletePrivateMessage, deletePrivateMessage,
unfollowRemotes,
waitUntil, waitUntil,
reportPrivateMessage,
unfollows,
} from "./shared"; } from "./shared";
let recipient_id: number; let recipient_id: number;
@ -21,7 +20,9 @@ beforeAll(async () => {
recipient_id = 3; recipient_id = 3;
}); });
afterAll(unfollows); afterAll(() => {
unfollowRemotes(alpha);
});
test("Create a private message", async () => { test("Create a private message", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id); let pmRes = await createPrivateMessage(alpha, recipient_id);
@ -108,42 +109,3 @@ test("Delete a private message", async () => {
betaPms1.private_messages.length, betaPms1.private_messages.length,
); );
}); });
test("Create a private message report", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil(
() => listPrivateMessages(beta),
m =>
!!m.private_messages.find(
e =>
e.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id,
),
);
let betaPm = betaPms1.private_messages[0];
expect(betaPm).toBeDefined();
// Make sure that only the recipient can report it, so this should fail
await expect(
reportPrivateMessage(
alpha,
pmRes.private_message_view.private_message.id,
"a reason",
),
).rejects.toStrictEqual(Error("couldnt_create_report"));
// This one should pass
let reason = "another reason";
let report = await reportPrivateMessage(
beta,
betaPm.private_message.id,
reason,
);
expect(report.private_message_report_view.private_message.id).toBe(
betaPm.private_message.id,
);
expect(report.private_message_report_view.private_message_report.reason).toBe(
reason,
);
});

View file

@ -4,16 +4,12 @@ import {
BlockInstance, BlockInstance,
BlockInstanceResponse, BlockInstanceResponse,
CommunityId, CommunityId,
CreatePrivateMessageReport,
DeleteImage,
EditCommunity,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetUnreadCountResponse, GetUnreadCountResponse,
InstanceId, InstanceId,
LemmyHttp, LemmyHttp,
PostView, PostView,
PrivateMessageReportResponse,
SuccessResponse, SuccessResponse,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
@ -79,26 +75,19 @@ import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDe
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails"; import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
import { ListingType } from "lemmy-js-client/dist/types/ListingType"; import { ListingType } from "lemmy-js-client/dist/types/ListingType";
export const fetchFunction = fetch; export let alphaUrl = "http://127.0.0.1:8541";
export const imageFetchLimit = 50; export let betaUrl = "http://127.0.0.1:8551";
export const sampleImage = export let gammaUrl = "http://127.0.0.1:8561";
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg"; export let deltaUrl = "http://127.0.0.1:8571";
export const sampleSite = "https://yahoo.com"; export let epsilonUrl = "http://127.0.0.1:8581";
export const alphaUrl = "http://127.0.0.1:8541"; export let alpha = new LemmyHttp(alphaUrl);
export const betaUrl = "http://127.0.0.1:8551"; export let beta = new LemmyHttp(betaUrl);
export const gammaUrl = "http://127.0.0.1:8561"; export let gamma = new LemmyHttp(gammaUrl);
export const deltaUrl = "http://127.0.0.1:8571"; export let delta = new LemmyHttp(deltaUrl);
export const epsilonUrl = "http://127.0.0.1:8581"; export let epsilon = new LemmyHttp(epsilonUrl);
export const alpha = new LemmyHttp(alphaUrl, { fetchFunction }); export let betaAllowedInstances = [
export const alphaImage = new LemmyHttp(alphaUrl);
export const beta = new LemmyHttp(betaUrl, { fetchFunction });
export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export const betaAllowedInstances = [
"lemmy-alpha", "lemmy-alpha",
"lemmy-gamma", "lemmy-gamma",
"lemmy-delta", "lemmy-delta",
@ -146,7 +135,6 @@ export async function setupLogins() {
resEpsilon, resEpsilon,
]); ]);
alpha.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` }); alpha.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
alphaImage.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
beta.setHeaders({ Authorization: `Bearer ${res[1].jwt ?? ""}` }); beta.setHeaders({ Authorization: `Bearer ${res[1].jwt ?? ""}` });
gamma.setHeaders({ Authorization: `Bearer ${res[2].jwt ?? ""}` }); gamma.setHeaders({ Authorization: `Bearer ${res[2].jwt ?? ""}` });
delta.setHeaders({ Authorization: `Bearer ${res[3].jwt ?? ""}` }); delta.setHeaders({ Authorization: `Bearer ${res[3].jwt ?? ""}` });
@ -183,10 +171,13 @@ export async function setupLogins() {
]; ];
await gamma.editSite(editSiteForm); await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"]; editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm); await delta.editSite(editSiteForm);
editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"];
await epsilon.editSite(editSiteForm);
// Create the main alpha/beta communities // Create the main alpha/beta communities
// Ignore thrown errors of duplicates // Ignore thrown errors of duplicates
try { try {
@ -205,20 +196,16 @@ export async function setupLogins() {
export async function createPost( export async function createPost(
api: LemmyHttp, api: LemmyHttp,
community_id: number, community_id: number,
url: string = "https://example.com/",
body = randomString(10),
// use example.com for consistent title and embed description // use example.com for consistent title and embed description
name: string = randomString(5), url: string = "https://example.com/",
alt_text = randomString(10),
custom_thumbnail: string | undefined = undefined,
): Promise<PostResponse> { ): Promise<PostResponse> {
let name = randomString(5);
let body = randomString(10);
let form: CreatePost = { let form: CreatePost = {
name, name,
url, url,
body, body,
alt_text,
community_id, community_id,
custom_thumbnail,
}; };
return api.createPost(form); return api.createPost(form);
} }
@ -235,21 +222,6 @@ export async function editPost(
return api.editPost(form); return api.editPost(form);
} }
export async function createPostWithThumbnail(
api: LemmyHttp,
community_id: number,
url: string,
custom_thumbnail: string,
): Promise<PostResponse> {
let form: CreatePost = {
name: randomString(10),
url,
community_id,
custom_thumbnail,
};
return api.createPost(form);
}
export async function deletePost( export async function deletePost(
api: LemmyHttp, api: LemmyHttp,
deleted: boolean, deleted: boolean,
@ -353,7 +325,6 @@ export async function getComments(
post_id: post_id, post_id: post_id,
type_: listingType, type_: listingType,
sort: "New", sort: "New",
limit: 50,
}; };
return api.getComments(form); return api.getComments(form);
} }
@ -364,13 +335,10 @@ export async function getUnreadCount(
return api.getUnreadCount(); return api.getUnreadCount();
} }
export async function getReplies( export async function getReplies(api: LemmyHttp): Promise<GetRepliesResponse> {
api: LemmyHttp,
unread_only: boolean = false,
): Promise<GetRepliesResponse> {
let form: GetReplies = { let form: GetReplies = {
sort: "New", sort: "New",
unread_only, unread_only: false,
}; };
return api.getReplies(form); return api.getReplies(form);
} }
@ -425,7 +393,7 @@ export async function banPersonFromSite(
let form: BanPerson = { let form: BanPerson = {
person_id, person_id,
ban, ban,
remove_data, remove_data: remove_data,
}; };
return api.banPerson(form); return api.banPerson(form);
} }
@ -553,7 +521,7 @@ export async function likeComment(
export async function createCommunity( export async function createCommunity(
api: LemmyHttp, api: LemmyHttp,
name_: string = randomString(10), name_: string = randomString(5),
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let description = "a sample description"; let description = "a sample description";
let form: CreateCommunity = { let form: CreateCommunity = {
@ -564,13 +532,6 @@ export async function createCommunity(
return api.createCommunity(form); return api.createCommunity(form);
} }
export async function editCommunity(
api: LemmyHttp,
form: EditCommunity,
): Promise<CommunityResponse> {
return api.editCommunity(form);
}
export async function getCommunity( export async function getCommunity(
api: LemmyHttp, api: LemmyHttp,
id: number, id: number,
@ -703,8 +664,8 @@ export async function saveUserSettingsBio(
export async function saveUserSettingsFederated( export async function saveUserSettingsFederated(
api: LemmyHttp, api: LemmyHttp,
): Promise<SuccessResponse> { ): Promise<SuccessResponse> {
let avatar = sampleImage; let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png";
let banner = sampleImage; let banner = "https://image.flaticon.com/icons/png/512/36/35896.png";
let bio = "a changed bio"; let bio = "a changed bio";
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
@ -770,7 +731,6 @@ export async function unfollowRemotes(
await Promise.all( await Promise.all(
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)), remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
); );
let siteRes = await getSite(api); let siteRes = await getSite(api);
return siteRes; return siteRes;
} }
@ -816,18 +776,6 @@ export async function reportComment(
return api.createCommentReport(form); return api.createCommentReport(form);
} }
export async function reportPrivateMessage(
api: LemmyHttp,
private_message_id: number,
reason: string,
): Promise<PrivateMessageReportResponse> {
let form: CreatePrivateMessageReport = {
private_message_id,
reason,
};
return api.createPrivateMessageReport(form);
}
export async function listCommentReports( export async function listCommentReports(
api: LemmyHttp, api: LemmyHttp,
): Promise<ListCommentReportsResponse> { ): Promise<ListCommentReportsResponse> {
@ -838,12 +786,9 @@ export async function listCommentReports(
export function getPosts( export function getPosts(
api: LemmyHttp, api: LemmyHttp,
listingType?: ListingType, listingType?: ListingType,
community_id?: number,
): Promise<GetPostsResponse> { ): Promise<GetPostsResponse> {
let form: GetPosts = { let form: GetPosts = {
type_: listingType, type_: listingType,
limit: 50,
community_id,
}; };
return api.getPosts(form); return api.getPosts(form);
} }
@ -895,50 +840,13 @@ export function randomString(length: number): string {
return result; return result;
} }
export async function deleteAllImages(api: LemmyHttp) {
const imagesRes = await api.listAllMedia({
limit: imageFetchLimit,
});
imagesRes.images;
Promise.all(
imagesRes.images
.map(image => {
const form: DeleteImage = {
token: image.local_image.pictrs_delete_token,
filename: image.local_image.pictrs_alias,
};
return form;
})
.map(form => api.deleteImage(form)),
);
}
export async function unfollows() { export async function unfollows() {
await Promise.all([ await Promise.all([
unfollowRemotes(alpha), unfollowRemotes(alpha),
unfollowRemotes(beta),
unfollowRemotes(gamma), unfollowRemotes(gamma),
unfollowRemotes(delta), unfollowRemotes(delta),
unfollowRemotes(epsilon), unfollowRemotes(epsilon),
]); ]);
await Promise.all([
purgeAllPosts(alpha),
purgeAllPosts(beta),
purgeAllPosts(gamma),
purgeAllPosts(delta),
purgeAllPosts(epsilon),
]);
}
export async function purgeAllPosts(api: LemmyHttp) {
// The best way to get all federated items, is to find the posts
let res = await api.getPosts({ type_: "All", limit: 50 });
await Promise.all(
Array.from(new Set(res.posts.map(p => p.post.id)))
.map(post_id => api.purgePost({ post_id }))
// Ignore errors
.map(p => p.catch(e => e)),
);
} }
export function getCommentParentId(comment: Comment): number | undefined { export function getCommentParentId(comment: Comment): number | undefined {
@ -949,7 +857,6 @@ export function getCommentParentId(comment: Comment): number | undefined {
if (split.length > 1) { if (split.length > 1) {
return Number(split[split.length - 2]); return Number(split[split.length - 2]);
} else { } else {
console.log(`Failed to extract comment parent id from ${comment.path}`);
return undefined; return undefined;
} }
} }

View file

@ -18,16 +18,11 @@ import {
saveUserSettings, saveUserSettings,
getPost, getPost,
getComments, getComments,
fetchFunction,
alphaImage,
unfollows,
saveUserSettingsBio,
} from "./shared"; } from "./shared";
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; import { LemmyHttp, SaveUserSettings } from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(unfollows);
let apShortname: string; let apShortname: string;
@ -49,7 +44,7 @@ test("Create user", async () => {
if (!site.my_user) { if (!site.my_user) {
throw "Missing site user"; throw "Missing site user";
} }
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
}); });
test("Set some user settings, check that they are federated", async () => { test("Set some user settings, check that they are federated", async () => {
@ -72,7 +67,7 @@ test("Delete user", async () => {
let user = await registerUser(alpha, alphaUrl); let user = await registerUser(alpha, alphaUrl);
// make a local post and comment // make a local post and comment
let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541")) let alphaCommunity = (await resolveCommunity(user, "!main@lemmy-alpha:8541"))
.community; .community;
if (!alphaCommunity) { if (!alphaCommunity) {
throw "Missing alpha community"; throw "Missing alpha community";
@ -119,7 +114,6 @@ test("Delete user", async () => {
test("Requests with invalid auth should be treated as unauthenticated", async () => { test("Requests with invalid auth should be treated as unauthenticated", async () => {
let invalid_auth = new LemmyHttp(alphaUrl, { let invalid_auth = new LemmyHttp(alphaUrl, {
headers: { Authorization: "Bearer foobar" }, headers: { Authorization: "Bearer foobar" },
fetchFunction,
}); });
let site = await getSite(invalid_auth); let site = await getSite(invalid_auth);
expect(site.my_user).toBeUndefined(); expect(site.my_user).toBeUndefined();
@ -138,75 +132,8 @@ test("Create user with Arabic name", async () => {
if (!site.my_user) { if (!site.my_user) {
throw "Missing site user"; throw "Missing site user";
} }
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
let alphaPerson = (await resolvePerson(alpha, apShortname)).person; let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
expect(alphaPerson).toBeDefined(); expect(alphaPerson).toBeDefined();
}); });
test("Create user with accept-language", async () => {
let lemmy_http = new LemmyHttp(alphaUrl, {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax
headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" },
});
let user = await registerUser(lemmy_http, alphaUrl);
let site = await getSite(user);
expect(site.my_user).toBeDefined();
expect(site.my_user?.local_user_view.local_user.interface_language).toBe(
"fr",
);
let langs = site.all_languages
.filter(a => site.my_user?.discussion_languages.includes(a.id))
.map(l => l.code);
// should have languages from accept header, as well as "undetermined"
// which is automatically enabled by backend
expect(langs).toStrictEqual(["und", "de", "en", "fr"]);
});
test("Set a new avatar, old avatar is deleted", async () => {
const listMediaRes = await alphaImage.listMedia();
expect(listMediaRes.images.length).toBe(0);
const upload_form1: UploadImage = {
image: Buffer.from("test1"),
};
const upload1 = await alphaImage.uploadImage(upload_form1);
expect(upload1.url).toBeDefined();
let form1 = {
avatar: upload1.url,
};
await saveUserSettings(alpha, form1);
const listMediaRes1 = await alphaImage.listMedia();
expect(listMediaRes1.images.length).toBe(1);
const upload_form2: UploadImage = {
image: Buffer.from("test2"),
};
const upload2 = await alphaImage.uploadImage(upload_form2);
expect(upload2.url).toBeDefined();
let form2 = {
avatar: upload2.url,
};
await saveUserSettings(alpha, form2);
// make sure only the new avatar is kept
const listMediaRes2 = await alphaImage.listMedia();
expect(listMediaRes2.images.length).toBe(1);
// Upload that same form2 avatar, make sure it isn't replaced / deleted
await saveUserSettings(alpha, form2);
// make sure only the new avatar is kept
const listMediaRes3 = await alphaImage.listMedia();
expect(listMediaRes3.images.length).toBe(1);
// Now try to save a user settings, with the icon missing,
// and make sure it doesn't clear the data, or delete the image
await saveUserSettingsBio(alpha);
let site = await getSite(alpha);
expect(site.my_user?.local_user_view.person.avatar).toBe(upload2.url);
// make sure only the new avatar is kept
const listMediaRes4 = await alphaImage.listMedia();
expect(listMediaRes4.images.length).toBe(1);
});

BIN
api_tests/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

3079
api_tests/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,89 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "LemmyNet"
repo = "lemmy"
# token = ""
[changelog]
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
## What's Changed
{%- if version %} in {{ version }}{%- endif -%}
{% for commit in commits %}
{% if commit.github.pr_title -%}
{%- set commit_message = commit.github.pr_title -%}
{%- else -%}
{%- set commit_message = commit.message -%}
{%- endif -%}
* {{ commit_message | split(pat="\n") | first | trim }}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
{% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %}
{%- endfor -%}
{%- if github -%}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
{% raw %}\n{% endraw -%}
## New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{%- endif -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# postprocessors
postprocessors = []
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# remove issue numbers from commits
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
]
commit_parsers = [{ field = "author.name", pattern = "renovate", skip = true }]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "[0-9].*"
# regex for skipping tags
skip_tags = "beta|alpha"
# regex for ignoring tags
ignore_tags = "rc"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View file

@ -34,45 +34,24 @@
# Name of the postgres database for lemmy # Name of the postgres database for lemmy
database: "string" database: "string"
# Maximum number of active sql connections # Maximum number of active sql connections
pool_size: 30 pool_size: 95
} }
# Settings related to activitypub federation
# Pictrs image server configuration. # Pictrs image server configuration.
pictrs: { pictrs: {
# Address where pictrs is available (for image hosting) # Address where pictrs is available (for image hosting)
url: "http://localhost:8080/" url: "http://localhost:8080/"
# Set a custom pictrs API key. ( Required for deleting images ) # Set a custom pictrs API key. ( Required for deleting images )
api_key: "string" api_key: "string"
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is # By default the thumbnails for external links are stored in pict-rs. This ensures that they
# equivalent to `image_mode: StoreLinkPreviews`. # can be reliably retrieved and can be resized using pict-rs APIs. However it also increases
# storage usage. In case this is disabled, the Opengraph image is directly returned as
# thumbnail.
# #
# To be removed in 0.20 # In some countries it is forbidden to copy preview images from newspaper articles and only
# hotlinking is allowed. If that is the case for your instance, make sure that this setting is
# disabled.
cache_external_link_previews: true cache_external_link_previews: true
# Specifies how to handle remote images, so that users don't have to connect directly to remote
# servers.
image_mode:
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
# Opengraph image is directly returned as thumbnail
"None"
# or
# Generate thumbnails for external post urls and store them persistently in pict-rs. This
# ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
# it also increases storage usage.
#
# This is the default behaviour, and also matches Lemmy 0.18.
"StoreLinkPreviews"
# or
# If enabled, all images from remote domains are rewritten to pass through
# `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
# servers, and decreases load on other servers. However it increases bandwidth use for the
# local server.
#
# Requires pict-rs 0.5
"ProxyAllImages"
# Timeout for uploading images to pictrs (in seconds) # Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30 upload_timeout: 30
} }

View file

@ -1,6 +1,5 @@
[package] [package]
name = "lemmy_api" name = "lemmy_api"
publish = false
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
@ -33,13 +32,12 @@ anyhow = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
url = { workspace = true } url = { workspace = true }
hound = "3.5.1" wav = "1.0.0"
sitemap-rs = "0.2.1" sitemap-rs = "0.2.0"
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } totp-rs = { version = "5.4.0", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.1" actix-web-httpauth = "0.8.1"
[dev-dependencies] [dev-dependencies]
serial_test = { workspace = true } serial_test = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
elementtree = "1.2.3" elementtree = "1.2.3"
pretty_assertions = { workspace = true }

View file

@ -9,17 +9,15 @@ 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::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn distinguish_comment( pub async fn distinguish_comment(
data: Json<DistinguishComment>, data: Json<DistinguishComment>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> Result<Json<CommentResponse>, LemmyError> {
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None) let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -28,11 +26,6 @@ pub async fn distinguish_comment(
) )
.await?; .await?;
// Verify that only the creator can distinguish
if local_user_view.person.id != orig_comment.creator.id {
Err(LemmyErrorType::NoCommentEditAllowed)?
}
// 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,
@ -56,8 +49,7 @@ pub async fn distinguish_comment(
data.comment_id, data.comment_id,
Some(local_user_view.person.id), Some(local_user_view.person.id),
) )
.await? .await?;
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::Likeable, traits::Likeable,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use std::ops::Deref; use std::ops::Deref;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -25,7 +25,7 @@ pub async fn like_comment(
data: Json<CreateCommentLike>, data: Json<CreateCommentLike>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> Result<Json<CommentResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let mut recipient_ids = Vec::<LocalUserId>::new(); let mut recipient_ids = Vec::<LocalUserId>::new();
@ -35,9 +35,7 @@ pub async fn like_comment(
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None) let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -48,10 +46,9 @@ pub async fn like_comment(
// Add parent poster or commenter to recipients // Add parent poster or commenter to recipients
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(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);
} }
@ -78,12 +75,12 @@ pub async fn like_comment(
} }
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment { SendActivityData::LikePostOrComment(
object_id: orig_comment.comment.ap_id, orig_comment.comment.ap_id,
actor: local_user_view.person.clone(), local_user_view.person.clone(),
community: orig_comment.community, orig_comment.community,
score: data.score, data.score,
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -1,36 +0,0 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
comment::{ListCommentLikes, ListCommentLikesResponse},
context::LemmyContext,
utils::is_mod_or_admin,
};
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a comment
#[tracing::instrument(skip(context))]
pub async fn list_comment_likes(
data: Query<ListCommentLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListCommentLikesResponse>> {
let comment_view = CommentView::read(
&mut context.pool(),
data.comment_id,
Some(local_user_view.person.id),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
comment_view.community.id,
)
.await?;
let comment_likes =
VoteView::list_for_comment(&mut context.pool(), data.comment_id, data.page, data.limit).await?;
Ok(Json(ListCommentLikesResponse { comment_likes }))
}

View file

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

View file

@ -8,14 +8,14 @@ use lemmy_db_schema::{
traits::Saveable, traits::Saveable,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn save_comment( pub async fn save_comment(
data: Json<SaveComment>, data: Json<SaveComment>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> Result<Json<CommentResponse>, LemmyError> {
let comment_saved_form = CommentSavedForm { let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id, comment_id: data.comment_id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
@ -33,9 +33,7 @@ pub async fn save_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)) let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -5,11 +5,7 @@ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport}, comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{ utils::{check_community_user_action, send_new_report_email_to_admins},
check_comment_deleted_or_removed,
check_community_user_action,
send_new_report_email_to_admins,
},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -19,7 +15,7 @@ use lemmy_db_schema::{
traits::Reportable, traits::Reportable,
}; };
use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Creates a comment report and notifies the moderators of the community /// Creates a comment report and notifies the moderators of the community
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -27,7 +23,7 @@ pub async fn create_comment_report(
data: Json<CreateCommentReport>, data: Json<CreateCommentReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReportResponse>> { ) -> Result<Json<CommentReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = data.reason.trim().to_string(); let reason = data.reason.trim().to_string();
@ -35,9 +31,7 @@ pub async fn create_comment_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, None) let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -46,9 +40,6 @@ pub async fn create_comment_report(
) )
.await?; .await?;
// Don't allow creating reports for removed / deleted comments
check_comment_deleted_or_removed(&comment_view.comment)?;
let report_form = CommentReportForm { let report_form = CommentReportForm {
creator_id: person_id, creator_id: person_id,
comment_id, comment_id,
@ -60,9 +51,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 {
@ -76,12 +66,12 @@ pub async fn create_comment_report(
} }
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::CreateReport { SendActivityData::CreateReport(
object_id: comment_view.comment.ap_id.inner().clone(), comment_view.comment.ap_id.inner().clone(),
actor: local_user_view.person, local_user_view.person,
community: comment_view.community, comment_view.community,
reason: data.reason.clone(), data.reason.clone(),
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::check_community_mod_of_any_or_admin_action, utils::check_community_mod_of_any_or_admin_action,
}; };
use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView}; use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
/// Lists comment reports for a community if an id is supplied /// Lists comment reports for a community if an id is supplied
/// or returns all comment reports for communities a user moderates /// or returns all comment reports for communities a user moderates
@ -14,9 +14,8 @@ pub async fn list_comment_reports(
data: Query<ListCommentReports>, data: Query<ListCommentReports>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListCommentReportsResponse>> { ) -> Result<Json<ListCommentReportsResponse>, LemmyError> {
let community_id = data.community_id; let community_id = data.community_id;
let comment_id = data.comment_id;
let unresolved_only = data.unresolved_only.unwrap_or_default(); let unresolved_only = data.unresolved_only.unwrap_or_default();
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
@ -25,7 +24,6 @@ pub async fn list_comment_reports(
let limit = data.limit; let limit = data.limit;
let comment_reports = CommentReportQuery { let comment_reports = CommentReportQuery {
community_id, community_id,
comment_id,
unresolved_only, unresolved_only,
page, page,
limit, limit,

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
use lemmy_db_views::structs::{CommentReportView, LocalUserView}; use lemmy_db_views::structs::{CommentReportView, LocalUserView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Resolves or unresolves a comment report and notifies the moderators of the community /// Resolves or unresolves a comment report and notifies the moderators of the community
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -14,18 +14,16 @@ pub async fn resolve_comment_report(
data: Json<ResolveCommentReport>, data: Json<ResolveCommentReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReportResponse>> { ) -> Result<Json<CommentReportResponse>, LemmyError> {
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.id,
true, false,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -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

@ -15,14 +15,14 @@ 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::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn add_mod_to_community( pub async fn add_mod_to_community(
data: Json<AddModToCommunity>, data: Json<AddModToCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<AddModToCommunityResponse>> { ) -> Result<Json<AddModToCommunityResponse>, LemmyError> {
let community_id = data.community_id; let community_id = data.community_id;
// Verify that only mods or admins can add mod // Verify that only mods or admins can add mod
@ -33,24 +33,10 @@ pub async fn add_mod_to_community(
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
let community = Community::read(&mut context.pool(), community_id) let community = Community::read(&mut context.pool(), community_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
// 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
// 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(
&mut context.pool(),
community.id,
local_user_view.person.id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)? Err(LemmyErrorType::NotAModerator)?
} }
}
// Update in local database // Update in local database
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
@ -83,12 +69,12 @@ pub async fn add_mod_to_community(
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::AddModToCommunity { SendActivityData::AddModToCommunity(
moderator: local_user_view.person, local_user_view.person,
community_id: data.community_id, data.community_id,
target: data.person_id, data.person_id,
added: data.added, data.added,
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -21,7 +21,7 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::validation::is_valid_body_field, utils::validation::is_valid_body_field,
}; };
@ -30,7 +30,7 @@ pub async fn ban_from_community(
data: Json<BanFromCommunity>, data: Json<BanFromCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BanFromCommunityResponse>> { ) -> Result<Json<BanFromCommunityResponse>, LemmyError> {
let banned_person_id = data.person_id; let banned_person_id = data.person_id;
let remove_data = data.remove_data.unwrap_or(false); let remove_data = data.remove_data.unwrap_or(false);
let expires = check_expire_time(data.expires)?; let expires = check_expire_time(data.expires)?;
@ -43,10 +43,7 @@ pub async fn ban_from_community(
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
is_valid_body_field(&data.reason, false)?;
if let Some(reason) = &data.reason {
is_valid_body_field(reason, false)?;
}
let community_user_ban_form = CommunityPersonBanForm { let community_user_ban_form = CommunityPersonBanForm {
community_id: data.community_id, community_id: data.community_id,
@ -92,17 +89,15 @@ 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(
moderator: local_user_view.person, local_user_view.person,
community_id: data.community_id, data.community_id,
target: person_view.person.clone(), person_view.person.clone(),
data: data.0.clone(), data.0.clone(),
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -14,14 +14,14 @@ use lemmy_db_schema::{
}; };
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::CommunityView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn block_community( pub async fn block_community(
data: Json<BlockCommunity>, data: Json<BlockCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BlockCommunityResponse>> { ) -> Result<Json<BlockCommunityResponse>, LemmyError> {
let community_id = data.community_id; let community_id = data.community_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_block_form = CommunityBlockForm { let community_block_form = CommunityBlockForm {
@ -51,9 +51,7 @@ pub async fn block_community(
} }
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FollowCommunity( SendActivityData::FollowCommunity(

View file

@ -15,17 +15,15 @@ use lemmy_db_schema::{
}; };
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::CommunityView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn follow_community( pub async fn follow_community(
data: Json<FollowCommunity>, data: Json<FollowCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> Result<Json<CommunityResponse>, LemmyError> {
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)?;
let mut community_follower_form = CommunityFollowerForm { let mut community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
@ -64,10 +62,7 @@ pub async fn follow_community(
let community_id = data.community_id; let community_id = data.community_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) CommunityView::read(&mut context.pool(), community_id, Some(person_id), 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?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {

View file

@ -15,14 +15,14 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn hide_community( pub async fn hide_community(
data: Json<HideCommunity>, data: Json<HideCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Verify its a admin (only admin can hide or unhide it) // Verify its a admin (only admin can hide or unhide it)
is_admin(&local_user_view)?; is_admin(&local_user_view)?;

View file

@ -15,7 +15,7 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType},
location_info, location_info,
}; };
@ -26,7 +26,7 @@ pub async fn transfer_community(
data: Json<TransferCommunity>, data: Json<TransferCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetCommunityResponse>> { ) -> Result<Json<GetCommunityResponse>, LemmyError> {
let community_id = data.community_id; let community_id = data.community_id;
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?;
@ -79,8 +79,8 @@ pub async fn transfer_community(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await? .await
.ok_or(LemmyErrorType::CouldntFindCommunity)?; .with_lemmy_type(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)

View file

@ -1,32 +1,16 @@
use activitypub_federation::config::Data;
use actix_web::{http::header::Header, HttpRequest}; use actix_web::{http::header::Header, HttpRequest};
use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
use captcha::Captcha; use captcha::Captcha;
use lemmy_api_common::{ use lemmy_api_common::{
claims::Claims, claims::Claims,
community::BanFromCommunity,
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
utils::{check_expire_time, check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
};
use lemmy_db_schema::{
source::{
community::{
CommunityFollower,
CommunityFollowerForm,
CommunityPersonBan,
CommunityPersonBanForm,
},
local_site::LocalSite,
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
person::Person,
},
traits::{Bannable, Crud, Followable},
}; };
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
utils::slurs::check_slurs, utils::slurs::check_slurs,
}; };
use std::io::Cursor; use std::io::Cursor;
@ -44,37 +28,31 @@ pub mod site;
pub mod sitemap; pub mod sitemap;
/// Converts the captcha to a base64 encoded wav audio file /// Converts the captcha to a base64 encoded wav audio file
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> { pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result<String, LemmyError> {
let letters = captcha.as_wav(); let letters = captcha.as_wav();
// Decode each wav file, concatenate the samples // Decode each wav file, concatenate the samples
let mut concat_samples: Vec<i16> = Vec::new(); let mut concat_samples: Vec<i16> = Vec::new();
let mut any_header: Option<hound::WavSpec> = None; let mut any_header: Option<wav::Header> = None;
for letter in letters { for letter in letters {
let mut cursor = Cursor::new(letter.unwrap_or_default()); let mut cursor = Cursor::new(letter.unwrap_or_default());
let reader = hound::WavReader::new(&mut cursor)?; let (header, samples) = wav::read(&mut cursor)?;
any_header = Some(reader.spec()); any_header = Some(header);
let samples16 = reader if let Some(samples16) = samples.as_sixteen() {
.into_samples::<i16>()
.collect::<Result<Vec<_>, _>>()
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
concat_samples.extend(samples16); concat_samples.extend(samples16);
} else {
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
}
} }
// Encode the concatenated result as a wav file // Encode the concatenated result as a wav file
let mut output_buffer = Cursor::new(vec![]); let mut output_buffer = Cursor::new(vec![]);
if let Some(header) = any_header { if let Some(header) = any_header {
let mut writer = hound::WavWriter::new(&mut output_buffer, header) wav::write(
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; header,
let mut writer16 = writer.get_i16_writer(concat_samples.len() as u32); &wav::BitDepth::Sixteen(concat_samples),
for sample in concat_samples { &mut output_buffer,
writer16.write_sample(sample); )
}
writer16
.flush()
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
writer
.finalize()
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
Ok(base64.encode(output_buffer.into_inner())) Ok(base64.encode(output_buffer.into_inner()))
@ -84,7 +62,7 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
} }
/// Check size of report /// Check size of report
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> LemmyResult<()> { pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
let slur_regex = &local_site_to_slur_regex(local_site); let slur_regex = &local_site_to_slur_regex(local_site);
check_slurs(reason, slur_regex)?; check_slurs(reason, slur_regex)?;
@ -97,15 +75,24 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Lemmy
} }
} }
pub fn read_auth_token(req: &HttpRequest) -> LemmyResult<Option<String>> { pub fn read_auth_token(req: &HttpRequest) -> Result<Option<String>, LemmyError> {
// Try reading jwt from auth header // Try reading jwt from auth header
if let Ok(header) = Authorization::<Bearer>::parse(req) { if let Ok(header) = Authorization::<Bearer>::parse(req) {
Ok(Some(header.as_ref().token().to_string())) Ok(Some(header.as_ref().token().to_string()))
} }
// If that fails, try to read from cookie // If that fails, try to read from cookie
else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) { else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) {
// ensure that its marked as httponly and secure
let secure = cookie.secure().unwrap_or_default();
let http_only = cookie.http_only().unwrap_or_default();
let is_debug_mode = cfg!(debug_assertions);
if !is_debug_mode && (!secure || !http_only) {
Err(LemmyError::from(LemmyErrorType::AuthCookieInsecure))
} else {
Ok(Some(cookie.value().to_string())) Ok(Some(cookie.value().to_string()))
} }
}
// Otherwise, there's no auth // Otherwise, there's no auth
else { else {
Ok(None) Ok(None)
@ -141,7 +128,11 @@ pub(crate) fn generate_totp_2fa_secret() -> String {
Secret::generate_secret().to_string() Secret::generate_secret().to_string()
} }
fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<TOTP> { pub(crate) fn build_totp_2fa(
site_name: &str,
username: &str,
secret: &str,
) -> Result<TOTP, LemmyError> {
let sec = Secret::Raw(secret.as_bytes().to_vec()); let sec = Secret::Raw(secret.as_bytes().to_vec());
let sec_bytes = sec let sec_bytes = sec
.to_bytes() .to_bytes()
@ -153,130 +144,37 @@ fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<T
1, 1,
30, 30,
sec_bytes, sec_bytes,
Some(hostname.to_string()), Some(site_name.to_string()),
username.to_string(), username.to_string(),
) )
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp) .with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
} }
/// Site bans are only federated for local users.
/// This is a problem, because site-banning non-local users will still leave content
/// they've posted to our local communities, on other servers.
///
/// So when doing a site ban for a non-local user, you need to federate/send a
/// community ban for every local community they've participated in.
/// See https://github.com/LemmyNet/lemmy/issues/4118
#[tracing::instrument(skip_all)]
pub(crate) async fn ban_nonlocal_user_from_local_communities(
local_user_view: &LocalUserView,
target: &Person,
ban: bool,
reason: &Option<String>,
remove_data: &Option<bool>,
expires: &Option<i64>,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
// Only run this code for federated users
if !target.local {
let ids = Person::list_local_community_ids(&mut context.pool(), target.id).await?;
for community_id in ids {
let expires_dt = check_expire_time(*expires)?;
// Ban / unban them from our local communities
let community_user_ban_form = CommunityPersonBanForm {
community_id,
person_id: target.id,
expires: Some(expires_dt),
};
if ban {
// Ignore all errors for these
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
.await
.ok();
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id,
person_id: target.id,
pending: false,
};
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
.await
.ok();
} else {
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
.await
.ok();
}
// Mod tables
let form = ModBanFromCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: target.id,
community_id,
reason: reason.clone(),
banned: Some(ban),
expires: expires_dt,
};
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
// Federate the ban from community
let ban_from_community = BanFromCommunity {
community_id,
person_id: target.id,
ban,
reason: reason.clone(),
remove_data: *remove_data,
expires: *expires,
};
ActivityChannel::submit_activity(
SendActivityData::BanFromCommunity {
moderator: local_user_view.person.clone(),
community_id,
target: target.clone(),
data: ban_from_community,
},
context,
)
.await?;
}
}
Ok(())
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn local_user_view_from_jwt( pub async fn local_user_view_from_jwt(
jwt: &str, jwt: &str,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<LocalUserView> { ) -> Result<LocalUserView, LemmyError> {
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 {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;
#[test] #[test]
fn test_build_totp() { fn test_build_totp() {
let generated_secret = generate_totp_2fa_secret(); let generated_secret = generate_totp_2fa_secret();
let totp = build_totp_2fa("lemmy.ml", "my_name", &generated_secret); let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
assert!(totp.is_ok()); assert!(totp.is_ok());
} }
} }

View file

@ -13,23 +13,23 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn add_admin( pub async fn add_admin(
data: Json<AddAdmin>, data: Json<AddAdmin>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<AddAdminResponse>> { ) -> Result<Json<AddAdminResponse>, LemmyError> {
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// 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)?; .with_lemmy_type(LemmyErrorType::ObjectNotLocal)?;
LocalUser::update( let added_admin = LocalUser::update(
&mut context.pool(), &mut context.pool(),
added_local_user.local_user.id, added_local_user.local_user.id,
&LocalUserUpdateForm { &LocalUserUpdateForm {
@ -43,7 +43,7 @@ pub async fn add_admin(
// Mod tables // Mod tables
let form = ModAddForm { let form = ModAddForm {
mod_person_id: local_user_view.person.id, mod_person_id: local_user_view.person.id,
other_person_id: added_local_user.person.id, other_person_id: added_admin.person_id,
removed: Some(!data.added), removed: Some(!data.added),
}; };

View file

@ -1,4 +1,3 @@
use crate::ban_nonlocal_user_from_local_communities;
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::{
@ -18,7 +17,7 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::validation::is_valid_body_field, utils::validation::is_valid_body_field,
}; };
@ -27,13 +26,11 @@ pub async fn ban_from_site(
data: Json<BanPerson>, data: Json<BanPerson>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BanPersonResponse>> { ) -> Result<Json<BanPersonResponse>, LemmyError> {
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
if let Some(reason) = &data.reason { is_valid_body_field(&data.reason, false)?;
is_valid_body_field(reason, false)?;
}
let expires = check_expire_time(data.expires)?; let expires = check_expire_time(data.expires)?;
@ -50,8 +47,8 @@ pub async fn ban_from_site(
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?; .with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
// 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(), data.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?;
} }
@ -64,7 +61,7 @@ pub async fn ban_from_site(
// Mod tables // Mod tables
let form = ModBanForm { let form = ModBanForm {
mod_person_id: local_user_view.person.id, mod_person_id: local_user_view.person.id,
other_person_id: person.id, other_person_id: data.person_id,
reason: data.reason.clone(), reason: data.reason.clone(),
banned: Some(data.ban), banned: Some(data.ban),
expires, expires,
@ -72,30 +69,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(), data.person_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities(
&local_user_view,
&person,
data.ban,
&data.reason,
&data.remove_data,
&data.expires,
&context,
)
.await?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::BanFromSite { SendActivityData::BanFromSite(
moderator: local_user_view.person, local_user_view.person,
banned_user: person_view.person.clone(), person_view.person.clone(),
reason: data.reason.clone(), data.0.clone(),
remove_data: data.remove_data, ),
ban: data.ban,
expires: data.expires,
},
&context, &context,
) )
.await?; .await?;

View file

@ -9,14 +9,14 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn block_person( pub async fn block_person(
data: Json<BlockPerson>, data: Json<BlockPerson>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BlockPersonResponse>> { ) -> Result<Json<BlockPersonResponse>, LemmyError> {
let target_id = data.person_id; let target_id = data.person_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
@ -30,12 +30,8 @@ pub async fn block_person(
target_id, target_id,
}; };
let target_user = LocalUserView::read_person(&mut context.pool(), target_id) let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await;
.await if target_user.map(|t| t.local_user.admin) == Ok(true) {
.ok()
.flatten();
if target_user.is_some_and(|t| t.local_user.admin) {
Err(LemmyErrorType::CantBlockAdmin)? Err(LemmyErrorType::CantBlockAdmin)?
} }
@ -49,9 +45,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

@ -11,7 +11,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken}; use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn change_password( pub async fn change_password(
@ -19,7 +19,7 @@ pub async fn change_password(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<LoginResponse>> { ) -> Result<Json<LoginResponse>, LemmyError> {
password_length_check(&data.new_password)?; password_length_check(&data.new_password)?;
// Make sure passwords match // Make sure passwords match

View file

@ -10,19 +10,18 @@ use lemmy_db_schema::source::{
login_token::LoginToken, login_token::LoginToken,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
}; };
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn change_password_after_reset( pub async fn change_password_after_reset(
data: Json<PasswordChangeAfterReset>, data: Json<PasswordChangeAfterReset>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Fetch the user_id from the token // Fetch the user_id from the token
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_from_token(&mut context.pool(), &token)
.await? .await
.ok_or(LemmyErrorType::TokenNotFound)? .map(|p| p.local_user_id)?;
.local_user_id;
password_length_check(&data.password)?; password_length_check(&data.password)?;

View file

@ -1,10 +1,17 @@
use crate::{build_totp_2fa, generate_totp_2fa_secret}; 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::{
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; context::LemmyContext,
person::GenerateTotpSecretResponse,
sensitive::Sensitive,
};
use lemmy_db_schema::{
source::local_user::{LocalUser, LocalUserUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorType};
/// 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]
/// to enable it. This can only be called if 2FA is currently disabled. /// to enable it. This can only be called if 2FA is currently disabled.
@ -12,10 +19,8 @@ use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn generate_totp_secret( pub async fn generate_totp_secret(
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GenerateTotpSecretResponse>> { ) -> Result<Json<GenerateTotpSecretResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()) let site_view = SiteView::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)?;
@ -37,6 +42,6 @@ pub async fn generate_totp_secret(
.await?; .await?;
Ok(Json(GenerateTotpSecretResponse { Ok(Json(GenerateTotpSecretResponse {
totp_secret_url: secret_url.into(), totp_secret_url: Sensitive::new(secret_url),
})) }))
} }

View file

@ -1,13 +1,5 @@
use crate::captcha_as_wav_base64; use crate::captcha_as_wav_base64;
use actix_web::{ use actix_web::web::{Data, Json};
http::{
header::{CacheControl, CacheDirective},
StatusCode,
},
web::{Data, Json},
HttpResponse,
HttpResponseBuilder,
};
use captcha::{gen, Difficulty}; use captcha::{gen, Difficulty};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
@ -17,16 +9,16 @@ use lemmy_db_schema::source::{
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm}, captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
local_site::LocalSite, local_site::LocalSite,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpResponse> { pub async fn get_captcha(
context: Data<LemmyContext>,
) -> Result<Json<GetCaptchaResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let mut res = HttpResponseBuilder::new(StatusCode::OK);
res.insert_header(CacheControl(vec![CacheDirective::NoStore]));
if !local_site.captcha_enabled { if !local_site.captcha_enabled {
return Ok(res.json(Json(GetCaptchaResponse { ok: None }))); return Ok(Json(GetCaptchaResponse { ok: None }));
} }
let captcha = gen(match local_site.captcha_difficulty.as_str() { let captcha = gen(match local_site.captcha_difficulty.as_str() {
@ -45,12 +37,11 @@ pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
// Stores the captcha item in the db // Stores the captcha item in the db
let captcha = CaptchaAnswer::insert(&mut context.pool(), &captcha_form).await?; let captcha = CaptchaAnswer::insert(&mut context.pool(), &captcha_form).await?;
let json = Json(GetCaptchaResponse { Ok(Json(GetCaptchaResponse {
ok: Some(CaptchaResponse { ok: Some(CaptchaResponse {
png, png,
wav, wav,
uuid: captcha.uuid.to_string(), uuid: captcha.uuid.to_string(),
}), }),
}); }))
Ok(res.json(json))
} }

View file

@ -2,12 +2,12 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin}; use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView; use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
pub async fn list_banned_users( pub async fn list_banned_users(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<BannedPersonsResponse>> { ) -> Result<Json<BannedPersonsResponse>, LemmyError> {
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;

View file

@ -2,12 +2,12 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::context::LemmyContext;
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::LemmyError;
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>>> { ) -> Result<Json<Vec<LoginToken>>, LemmyError> {
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(logins))

View file

@ -1,25 +0,0 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListMedia, ListMediaResponse},
};
use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn list_media(
data: Query<ListMedia>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListMediaResponse>> {
let page = data.page;
let limit = data.limit;
let images = LocalImageView::get_all_paged_by_local_user_id(
&mut context.pool(),
local_user_view.local_user.id,
page,
limit,
)
.await?;
Ok(Json(ListMediaResponse { images }))
}

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,
@ -16,24 +16,22 @@ use lemmy_db_schema::{
RegistrationMode, 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::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn login( pub async fn login(
data: Json<Login>, data: Json<Login>,
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> Result<Json<LoginResponse>, LemmyError> {
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)?; .with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
// Verify the password // Verify the password
let valid: bool = verify( let valid: bool = verify(
@ -45,18 +43,22 @@ pub async fn login(
Err(LemmyErrorType::IncorrectLogin)? Err(LemmyErrorType::IncorrectLogin)?
} }
check_user_valid(&local_user_view.person)?; check_user_valid(&local_user_view.person)?;
check_email_verified(&local_user_view, &site_view)?;
// Check if the user's email is verified if email verification is turned on
// However, skip checking verification if the user is an admin
if !local_user_view.local_user.admin
&& site_view.local_site.require_email_verification
&& !local_user_view.local_user.email_verified
{
Err(LemmyErrorType::EmailNotVerified)?
}
check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool()) check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool())
.await?; .await?;
// Check the totp if enabled // Check the totp if enabled
if local_user_view.local_user.totp_2fa_enabled { if local_user_view.local_user.totp_2fa_enabled {
check_totp_2fa_valid( check_totp_2fa_valid(&local_user_view, &data.totp_2fa_token, &site_view.site.name)?;
&local_user_view,
&data.totp_2fa_token,
&context.settings().hostname,
)?;
} }
let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?; let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?;
@ -72,7 +74,7 @@ async fn check_registration_application(
local_user_view: &LocalUserView, local_user_view: &LocalUserView,
local_site: &LocalSite, local_site: &LocalSite,
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> Result<(), LemmyError> {
if (local_site.registration_mode == RegistrationMode::RequireApplication if (local_site.registration_mode == RegistrationMode::RequireApplication
|| local_site.registration_mode == RegistrationMode::Closed) || local_site.registration_mode == RegistrationMode::Closed)
&& !local_user_view.local_user.accepted_application && !local_user_view.local_user.accepted_application
@ -81,9 +83,7 @@ async fn check_registration_application(
// Fetch the registration application. If no admin id is present its still pending. Otherwise it // Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied). // was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id; let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id) let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
if registration.admin_id.is_some() { if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else { } else {

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;
@ -10,7 +7,6 @@ pub mod generate_totp_secret;
pub mod get_captcha; pub mod get_captcha;
pub mod list_banned; pub mod list_banned;
pub mod list_logins; pub mod list_logins;
pub mod list_media;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod notifications; pub mod notifications;
@ -20,15 +16,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

@ -5,14 +5,14 @@ use lemmy_api_common::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::person_mention_view::PersonMentionQuery; use lemmy_db_views_actor::person_mention_view::PersonMentionQuery;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_mentions( pub async fn list_mentions(
data: Query<GetPersonMentions>, data: Query<GetPersonMentions>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetPersonMentionsResponse>> { ) -> Result<Json<GetPersonMentionsResponse>, LemmyError> {
let sort = data.sort; let sort = data.sort;
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;

View file

@ -5,14 +5,14 @@ use lemmy_api_common::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery; use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_replies( pub async fn list_replies(
data: Query<GetReplies>, data: Query<GetReplies>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetRepliesResponse>> { ) -> Result<Json<GetRepliesResponse>, LemmyError> {
let sort = data.sort; let sort = data.sort;
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;

View file

@ -6,13 +6,13 @@ use lemmy_db_schema::source::{
private_message::PrivateMessage, private_message::PrivateMessage,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn mark_all_notifications_read( pub async fn mark_all_notifications_read(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetRepliesResponse>> { ) -> Result<Json<GetRepliesResponse>, LemmyError> {
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
// Mark all comment_replies as read // Mark all comment_replies as read

View file

@ -9,18 +9,16 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonMentionView; use lemmy_db_views_actor::structs::PersonMentionView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn mark_person_mention_as_read( pub async fn mark_person_mention_as_read(
data: Json<MarkPersonMentionAsRead>, data: Json<MarkPersonMentionAsRead>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> { ) -> Result<Json<PersonMentionResponse>, LemmyError> {
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

@ -9,18 +9,16 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommentReplyView; use lemmy_db_views_actor::structs::CommentReplyView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn mark_reply_as_read( pub async fn mark_reply_as_read(
data: Json<MarkCommentReplyAsRead>, data: Json<MarkCommentReplyAsRead>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> { ) -> Result<Json<CommentReplyResponse>, LemmyError> {
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

@ -2,21 +2,18 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView}; use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn unread_count( pub async fn unread_count(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetUnreadCountResponse>> { ) -> Result<Json<GetUnreadCountResponse>, LemmyError> {
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let replies = let replies = CommentReplyView::get_unread_replies(&mut context.pool(), person_id).await?;
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?;
let mentions = let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?;
PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user)
.await?;
let private_messages = let private_messages =
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?; PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;

View file

@ -10,14 +10,14 @@ use lemmy_db_views::structs::{
PostReportView, PostReportView,
PrivateMessageReportView, PrivateMessageReportView,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn report_count( pub async fn report_count(
data: Query<GetReportCount>, data: Query<GetReportCount>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetReportCountResponse>> { ) -> Result<Json<GetReportCountResponse>, LemmyError> {
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let admin = local_user_view.local_user.admin; let admin = local_user_view.local_user.admin;
let community_id = data.community_id; let community_id = data.community_id;

View file

@ -1,4 +1,3 @@
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,
@ -6,8 +5,9 @@ use lemmy_api_common::{
utils::send_password_reset_email, utils::send_password_reset_email,
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn reset_password( pub async fn reset_password(
@ -17,13 +17,18 @@ 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)?; .with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
let site_view = SiteView::read_local(&mut context.pool()) // Check for too many attempts (to limit potential abuse)
.await? let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count(
.ok_or(LemmyErrorType::LocalSiteNotSetup)?; &mut context.pool(),
check_email_verified(&local_user_view, &site_view)?; local_user_view.local_user.id,
)
.await?;
if recent_resets_count >= 3 {
Err(LemmyErrorType::PasswordResetLimitReached)?
}
// Email the pure token to the user. // Email the pure token to the user.
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?; send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?;

View file

@ -1,70 +1,45 @@
use activitypub_federation::config::Data; use actix_web::web::{Data, Json};
use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::SaveUserSettings, person::SaveUserSettings,
request::replace_image, utils::send_verification_email,
utils::{
get_url_blocklist,
local_site_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_api,
send_verification_email,
},
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
actor_language::LocalUserLanguage, actor_language::LocalUserLanguage,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::{diesel_string_update, diesel_url_update}, utils::{diesel_option_overwrite, diesel_option_overwrite_to_url},
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorType},
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id}, utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
}; };
use std::ops::Deref;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn save_user_settings( pub async fn save_user_settings(
data: Json<SaveUserSettings>, data: Json<SaveUserSettings>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
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 avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let url_blocklist = get_url_blocklist(&context).await?; let banner = diesel_option_overwrite_to_url(&data.banner)?;
let bio = diesel_string_update( let bio = diesel_option_overwrite(data.bio.clone());
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context) let display_name = diesel_option_overwrite(data.display_name.clone());
.await? let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
.as_deref(),
);
let avatar = diesel_url_update(data.avatar.as_deref())?;
replace_image(&avatar, &local_user_view.person.avatar, &context).await?;
let avatar = proxy_image_link_opt_api(avatar, &context).await?;
let banner = diesel_url_update(data.banner.as_deref())?;
replace_image(&banner, &local_user_view.person.banner, &context).await?;
let banner = proxy_image_link_opt_api(banner, &context).await?;
let display_name = diesel_string_update(data.display_name.as_deref());
let matrix_user_id = diesel_string_update(data.matrix_user_id.as_deref());
let email_deref = data.email.as_deref().map(str::to_lowercase); let email_deref = data.email.as_deref().map(str::to_lowercase);
let email = diesel_string_update(email_deref.as_deref()); let email = diesel_option_overwrite(email_deref.clone());
if let Some(Some(email)) = &email { if let Some(Some(email)) = &email {
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
// 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 != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? { if LocalUser::is_email_taken(&mut context.pool(), email).await? {
return Err(LemmyErrorType::EmailAlreadyExists)?; return Err(LemmyErrorType::EmailAlreadyExists)?;
} }
@ -78,8 +53,7 @@ pub async fn save_user_settings(
} }
} }
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
// value
if let Some(email) = &email { if let Some(email) = &email {
if email.is_none() && site_view.local_site.require_email_verification { if email.is_none() && site_view.local_site.require_email_verification {
Err(LemmyErrorType::EmailRequired)? Err(LemmyErrorType::EmailRequired)?
@ -149,17 +123,11 @@ pub async fn save_user_settings(
..Default::default() ..Default::default()
}; };
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?; // Ignore errors, because 'no fields updated' will return an error.
// https://github.com/LemmyNet/lemmy/issues/4076
// Update the vote display modes LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm { .await
score: data.show_scores, .ok();
upvotes: data.show_upvotes,
downvotes: data.show_downvotes,
upvote_percentage: data.show_upvote_percentage,
};
LocalUserVoteDisplayMode::update(&mut context.pool(), local_user_id, &vote_display_modes_form)
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -4,9 +4,12 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{UpdateTotp, UpdateTotpResponse}, person::{UpdateTotp, UpdateTotpResponse},
}; };
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; use lemmy_db_schema::{
use lemmy_db_views::structs::LocalUserView; source::local_user::{LocalUser, LocalUserUpdateForm},
use lemmy_utils::error::LemmyResult; traits::Crud,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::LemmyError;
/// Enable or disable two-factor-authentication. The current setting is determined from /// Enable or disable two-factor-authentication. The current setting is determined from
/// [LocalUser.totp_2fa_enabled]. /// [LocalUser.totp_2fa_enabled].
@ -21,11 +24,13 @@ pub async fn update_totp(
data: Json<UpdateTotp>, data: Json<UpdateTotp>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<UpdateTotpResponse>> { ) -> Result<Json<UpdateTotpResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_totp_2fa_valid( check_totp_2fa_valid(
&local_user_view, &local_user_view,
&Some(data.totp_token.clone()), &Some(data.totp_token.clone()),
&context.settings().hostname, &site_view.site.name,
)?; )?;
// toggle the 2fa setting // toggle the 2fa setting

View file

@ -4,7 +4,7 @@ use actix_web::{
HttpRequest, HttpRequest,
}; };
use lemmy_api_common::{context::LemmyContext, SuccessResponse}; use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorType};
/// Returns an error message if the auth token is invalid for any reason. Necessary because other /// Returns an error message if the auth token is invalid for any reason. Necessary because other
/// endpoints silently treat any call with invalid auth as unauthenticated. /// endpoints silently treat any call with invalid auth as unauthenticated.
@ -12,7 +12,7 @@ use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn validate_auth( pub async fn validate_auth(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
let jwt = read_auth_token(&req)?; let jwt = read_auth_token(&req)?;
if let Some(jwt) = jwt { if let Some(jwt) = jwt {
local_user_view_from_jwt(&jwt, &context).await?; local_user_view_from_jwt(&jwt, &context).await?;

View file

@ -9,23 +9,23 @@ use lemmy_db_schema::{
source::{ source::{
email_verification::EmailVerification, email_verification::EmailVerification,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
person::Person,
}, },
traits::Crud,
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::SiteView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, 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)?; .with_lemmy_type(LemmyErrorType::TokenNotFound)?;
let form = LocalUserUpdateForm { let form = LocalUserUpdateForm {
// necessary in case this is a new signup // necessary in case this is a new signup
@ -36,7 +36,7 @@ pub async fn verify_email(
}; };
let local_user_id = verification.local_user_id; let local_user_id = verification.local_user_id;
LocalUser::update(&mut context.pool(), local_user_id, &form).await?; let local_user = LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
@ -44,15 +44,8 @@ pub async fn verify_email(
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
&& site_view.local_site.application_email_admins && site_view.local_site.application_email_admins
{ {
let local_user = LocalUserView::read(&mut context.pool(), local_user_id) let person = Person::read(&mut context.pool(), local_user.person_id).await?;
.await? send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings())
.ok_or(LemmyErrorType::CouldntFindPerson)?;
send_new_applicant_email_to_admins(
&local_user.person.name,
&mut context.pool(),
context.settings(),
)
.await?; .await?;
} }

View file

@ -16,18 +16,16 @@ 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::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn feature_post( pub async fn feature_post(
data: Json<FeaturePost>, data: Json<FeaturePost>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> Result<Json<PostResponse>, LemmyError> {
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)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View file

@ -1,22 +1,17 @@
use actix_web::web::{Data, Json, Query}; use actix_web::web::{Data, Json};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetSiteMetadata, GetSiteMetadataResponse}, post::{GetSiteMetadata, GetSiteMetadataResponse},
request::fetch_link_metadata, request::fetch_site_metadata,
}; };
use lemmy_utils::{ use lemmy_utils::error::LemmyError;
error::{LemmyErrorExt, LemmyResult},
LemmyErrorType,
};
use url::Url;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_link_metadata( pub async fn get_link_metadata(
data: Query<GetSiteMetadata>, data: Json<GetSiteMetadata>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetSiteMetadataResponse>> { ) -> Result<Json<GetSiteMetadataResponse>, LemmyError> {
let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?; let metadata = fetch_site_metadata(context.client(), &data.url).await?;
let metadata = fetch_link_metadata(&url, &context).await?;
Ok(Json(GetSiteMetadataResponse { metadata })) Ok(Json(GetSiteMetadataResponse { metadata }))
} }

View file

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

View file

@ -21,7 +21,7 @@ use lemmy_db_schema::{
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use std::ops::Deref; use std::ops::Deref;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -29,7 +29,7 @@ pub async fn like_post(
data: Json<CreatePostLike>, data: Json<CreatePostLike>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> Result<Json<PostResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
// Don't do a downvote if site has downvotes disabled // Don't do a downvote if site has downvotes disabled
@ -38,9 +38,7 @@ pub async fn like_post(
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post_id = data.post_id;
let post = Post::read(&mut context.pool(), post_id) let post = Post::read(&mut context.pool(), post_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -68,19 +66,16 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
} }
// Mark the post as read
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, post.ap_id,
actor: local_user_view.person.clone(), local_user_view.person.clone(),
community, Community::read(&mut context.pool(), post.community_id).await?,
score: data.score, data.score,
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -1,32 +0,0 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{ListPostLikes, ListPostLikesResponse},
utils::is_mod_or_admin,
};
use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a post
#[tracing::instrument(skip(context))]
pub async fn list_post_likes(
data: Query<ListPostLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostLikesResponse>> {
let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,
post.community_id,
)
.await?;
let post_likes =
VoteView::list_for_post(&mut context.pool(), data.post_id, data.page, data.limit).await?;
Ok(Json(ListPostLikesResponse { post_likes }))
}

View file

@ -15,18 +15,16 @@ 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::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn lock_post( pub async fn lock_post(
data: Json<LockPost>, data: Json<LockPost>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> Result<Json<PostResponse>, LemmyError> {
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)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View file

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse}; use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead; use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet; use std::collections::HashSet;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -10,8 +10,15 @@ pub async fn mark_post_as_read(
data: Json<MarkPostAsRead>, data: Json<MarkPostAsRead>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
let post_ids = HashSet::from_iter(data.post_ids.clone()); let mut post_ids = HashSet::new();
if let Some(post_ids_) = &data.post_ids {
post_ids.extend(post_ids_.iter().cloned());
}
if let Some(post_id) = data.post_id {
post_ids.insert(post_id);
}
if post_ids.len() > MAX_API_PARAM_ELEMENTS { if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?; Err(LemmyErrorType::TooManyItems)?;

View file

@ -1,8 +1,6 @@
pub mod feature; pub mod feature;
pub mod get_link_metadata; pub mod get_link_metadata;
pub mod hide;
pub mod like; pub mod like;
pub mod list_post_likes;
pub mod lock; pub mod lock;
pub mod mark_read; pub mod mark_read;
pub mod save; pub mod save;

View file

@ -9,14 +9,14 @@ use lemmy_db_schema::{
traits::Saveable, traits::Saveable,
}; };
use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn save_post( pub async fn save_post(
data: Json<SavePost>, data: Json<SavePost>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> Result<Json<PostResponse>, LemmyError> {
let post_saved_form = PostSavedForm { let post_saved_form = PostSavedForm {
post_id: data.post_id, post_id: data.post_id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
@ -34,10 +34,9 @@ pub async fn save_post(
let post_id = data.post_id; let post_id = data.post_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false) let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))

View file

@ -5,11 +5,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{CreatePostReport, PostReportResponse}, post::{CreatePostReport, PostReportResponse},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{ utils::{check_community_user_action, send_new_report_email_to_admins},
check_community_user_action,
check_post_deleted_or_removed,
send_new_report_email_to_admins,
},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
@ -19,7 +15,7 @@ use lemmy_db_schema::{
traits::Reportable, traits::Reportable,
}; };
use lemmy_db_views::structs::{LocalUserView, PostReportView, PostView}; use lemmy_db_views::structs::{LocalUserView, PostReportView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Creates a post report and notifies the moderators of the community /// Creates a post report and notifies the moderators of the community
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -27,7 +23,7 @@ pub async fn create_post_report(
data: Json<CreatePostReport>, data: Json<CreatePostReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostReportResponse>> { ) -> Result<Json<PostReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = data.reason.trim().to_string(); let reason = data.reason.trim().to_string();
@ -35,9 +31,7 @@ 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,
@ -46,8 +40,6 @@ pub async fn create_post_report(
) )
.await?; .await?;
check_post_deleted_or_removed(&post_view.post)?;
let report_form = PostReportForm { let report_form = PostReportForm {
creator_id: person_id, creator_id: person_id,
post_id, post_id,
@ -61,9 +53,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 {
@ -77,12 +67,12 @@ pub async fn create_post_report(
} }
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::CreateReport { SendActivityData::CreateReport(
object_id: post_view.post.ap_id.inner().clone(), post_view.post.ap_id.inner().clone(),
actor: local_user_view.person, local_user_view.person,
community: post_view.community, post_view.community,
reason: data.reason.clone(), data.reason.clone(),
}, ),
&context, &context,
) )
.await?; .await?;

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::check_community_mod_of_any_or_admin_action, utils::check_community_mod_of_any_or_admin_action,
}; };
use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView}; use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
/// Lists post reports for a community if an id is supplied /// Lists post reports for a community if an id is supplied
/// or returns all post reports for communities a user moderates /// or returns all post reports for communities a user moderates
@ -14,9 +14,8 @@ pub async fn list_post_reports(
data: Query<ListPostReports>, data: Query<ListPostReports>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostReportsResponse>> { ) -> Result<Json<ListPostReportsResponse>, LemmyError> {
let community_id = data.community_id; let community_id = data.community_id;
let post_id = data.post_id;
let unresolved_only = data.unresolved_only.unwrap_or_default(); let unresolved_only = data.unresolved_only.unwrap_or_default();
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
@ -25,7 +24,6 @@ pub async fn list_post_reports(
let limit = data.limit; let limit = data.limit;
let post_reports = PostReportQuery { let post_reports = PostReportQuery {
community_id, community_id,
post_id,
unresolved_only, unresolved_only,
page, page,
limit, limit,

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable};
use lemmy_db_views::structs::{LocalUserView, PostReportView}; use lemmy_db_views::structs::{LocalUserView, PostReportView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
/// Resolves or unresolves a post report and notifies the moderators of the community /// Resolves or unresolves a post report and notifies the moderators of the community
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -14,18 +14,16 @@ pub async fn resolve_post_report(
data: Json<ResolvePostReport>, data: Json<ResolvePostReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostReportResponse>> { ) -> Result<Json<PostReportResponse>, LemmyError> {
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.id,
true, false,
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
@ -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

@ -8,19 +8,17 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn mark_pm_as_read( pub async fn mark_pm_as_read(
data: Json<MarkPrivateMessageAsRead>, data: Json<MarkPrivateMessageAsRead>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> Result<Json<PrivateMessageResponse>, LemmyError> {
// 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

@ -14,14 +14,14 @@ use lemmy_db_schema::{
traits::{Crud, Reportable}, traits::{Crud, Reportable},
}; };
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn create_pm_report( pub async fn create_pm_report(
data: Json<CreatePrivateMessageReport>, data: Json<CreatePrivateMessageReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PrivateMessageReportResponse>> { ) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = data.reason.trim().to_string(); let reason = data.reason.trim().to_string();
@ -29,14 +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
if person_id != private_message.recipient_id {
Err(LemmyErrorType::CouldntCreateReport)?
}
let report_form = PrivateMessageReportForm { let report_form = PrivateMessageReportForm {
creator_id: person_id, creator_id: person_id,
@ -49,9 +42,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

@ -8,14 +8,14 @@ use lemmy_db_views::{
private_message_report_view::PrivateMessageReportQuery, private_message_report_view::PrivateMessageReportQuery,
structs::LocalUserView, structs::LocalUserView,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_pm_reports( pub async fn list_pm_reports(
data: Query<ListPrivateMessageReports>, data: Query<ListPrivateMessageReports>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPrivateMessageReportsResponse>> { ) -> Result<Json<ListPrivateMessageReportsResponse>, LemmyError> {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let unresolved_only = data.unresolved_only.unwrap_or_default(); let unresolved_only = data.unresolved_only.unwrap_or_default();

View file

@ -6,14 +6,14 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable}; use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView}; use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn resolve_pm_report( pub async fn resolve_pm_report(
data: Json<ResolvePrivateMessageReport>, data: Json<ResolvePrivateMessageReport>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PrivateMessageReportResponse>> { ) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let report_id = data.report_id; let report_id = data.report_id;
@ -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

@ -9,20 +9,16 @@ use lemmy_db_schema::{
traits::Blockable, traits::Blockable,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn block_instance( pub async fn block_instance(
data: Json<BlockInstance>, data: Json<BlockInstance>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<BlockInstanceResponse>> { ) -> Result<Json<BlockInstanceResponse>, LemmyError> {
let instance_id = data.instance_id; let instance_id = data.instance_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
if local_user_view.person.instance_id == instance_id {
return Err(LemmyErrorType::CantBlockLocalInstance)?;
}
let instance_block_form = InstanceBlockForm { let instance_block_form = InstanceBlockForm {
person_id, person_id,
instance_id, instance_id,

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::LemmyError;
#[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>> { ) -> Result<Json<GetFederatedInstancesResponse>, LemmyError> {
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

@ -4,7 +4,6 @@ use lemmy_db_schema::{
source::{ source::{
actor_language::SiteLanguage, actor_language::SiteLanguage,
language::Language, language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm}, moderator::{ModAdd, ModAddForm},
tagline::Tagline, tagline::Tagline,
@ -14,15 +13,15 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; use lemmy_db_views::structs::{CustomEmojiView, 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::{LemmyError, LemmyErrorType},
VERSION, version,
}; };
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn leave_admin( pub async fn leave_admin(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetSiteResponse>> { ) -> Result<Json<GetSiteResponse>, LemmyError> {
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Make sure there isn't just one admin (so if one leaves, there will still be one left) // Make sure there isn't just one admin (so if one leaves, there will still be one left)
@ -55,9 +54,7 @@ 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?;
@ -65,17 +62,15 @@ pub async fn leave_admin(
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?; let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis = let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
Ok(Json(GetSiteResponse { Ok(Json(GetSiteResponse {
site_view, site_view,
admins, admins,
version: VERSION.to_string(), version: version::VERSION.to_string(),
my_user: None, my_user: None,
all_languages, all_languages,
discussion_languages, discussion_languages,
taglines, taglines,
custom_emojis, custom_emojis,
blocked_urls,
})) }))
} }

View file

@ -1,23 +0,0 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListMedia, ListMediaResponse},
utils::is_admin,
};
use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn list_all_media(
data: Query<ListMedia>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListMediaResponse>> {
// Only let admins view all media
is_admin(&local_user_view)?;
let page = data.page;
let limit = data.limit;
let images = LocalImageView::get_all(&mut context.pool(), page, limit).await?;
Ok(Json(ListMediaResponse { images }))
}

View file

@ -1,7 +1,6 @@
pub mod block; pub mod block;
pub mod federated_instances; pub mod federated_instances;
pub mod leave_admin; pub mod leave_admin;
pub mod list_all_media;
pub mod mod_log; pub mod mod_log;
pub mod purge; pub mod purge;
pub mod registration_applications; pub mod registration_applications;

View file

@ -24,7 +24,7 @@ use lemmy_db_views_moderator::structs::{
ModTransferCommunityView, ModTransferCommunityView,
ModlogListParams, ModlogListParams,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
use ModlogActionType::*; use ModlogActionType::*;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -32,7 +32,7 @@ pub async fn get_mod_log(
data: Query<GetModlog>, data: Query<GetModlog>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetModlogResponse>> { ) -> Result<Json<GetModlogResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?; check_private_instance(&local_user_view, &local_site)?;
@ -55,15 +55,10 @@ pub async fn get_mod_log(
data.mod_person_id data.mod_person_id
}; };
let other_person_id = data.other_person_id; let other_person_id = data.other_person_id;
let post_id = data.post_id;
let comment_id = data.comment_id;
let params = ModlogListParams { let params = ModlogListParams {
community_id, community_id,
mod_person_id, mod_person_id,
other_person_id, other_person_id,
post_id,
comment_id,
page: data.page, page: data.page,
limit: data.limit, limit: data.limit,
hide_modlog_names, hide_modlog_names,

View file

@ -1,8 +1,6 @@
use activitypub_federation::config::Data; use actix_web::web::{Data, Json};
use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
site::PurgeComment, site::PurgeComment,
utils::is_admin, utils::is_admin,
SuccessResponse, SuccessResponse,
@ -14,26 +12,24 @@ use lemmy_db_schema::{
}, },
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_comment( pub async fn purge_comment(
data: Json<PurgeComment>, data: Json<PurgeComment>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
// Read the comment to get the post_id and community // Read the comment to get the post_id
let comment_view = CommentView::read(&mut context.pool(), comment_id, None) let comment = Comment::read(&mut context.pool(), comment_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let post_id = comment_view.comment.post_id; let post_id = comment.post_id;
// TODO read comments for pictrs images and purge them // TODO read comments for pictrs images and purge them
@ -45,18 +41,8 @@ pub async fn purge_comment(
reason: data.reason.clone(), reason: data.reason.clone(),
post_id, post_id,
}; };
AdminPurgeComment::create(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity( AdminPurgeComment::create(&mut context.pool(), &form).await?;
SendActivityData::RemoveComment {
comment: comment_view.comment,
moderator: local_user_view.person.clone(),
community: comment_view.community,
reason: data.reason.clone(),
},
&context,
)
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -1,9 +1,7 @@
use activitypub_federation::config::Data; use actix_web::web::{Data, Json};
use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
request::purge_image_from_pictrs, request::purge_image_from_pictrs,
send_activity::{ActivityChannel, SendActivityData},
site::PurgeCommunity, site::PurgeCommunity,
utils::{is_admin, purge_image_posts_for_community}, utils::{is_admin, purge_image_posts_for_community},
SuccessResponse, SuccessResponse,
@ -16,51 +14,41 @@ 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::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_community( pub async fn purge_community(
data: Json<PurgeCommunity>, data: Json<PurgeCommunity>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let community_id = data.community_id;
// 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(), community_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if let Some(banner) = &community.banner { if let Some(banner) = community.banner {
purge_image_from_pictrs(banner, &context).await.ok(); purge_image_from_pictrs(&banner, &context).await.ok();
} }
if let Some(icon) = &community.icon { if let Some(icon) = community.icon {
purge_image_from_pictrs(icon, &context).await.ok(); purge_image_from_pictrs(&icon, &context).await.ok();
} }
purge_image_posts_for_community(data.community_id, &context).await?; purge_image_posts_for_community(community_id, &context).await?;
Community::delete(&mut context.pool(), data.community_id).await?; Community::delete(&mut context.pool(), community_id).await?;
// Mod tables // Mod tables
let form = AdminPurgeCommunityForm { let form = AdminPurgeCommunityForm {
admin_person_id: local_user_view.person.id, admin_person_id: local_user_view.person.id,
reason: data.reason.clone(), reason: data.reason.clone(),
}; };
AdminPurgeCommunity::create(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity( AdminPurgeCommunity::create(&mut context.pool(), &form).await?;
SendActivityData::RemoveCommunity {
moderator: local_user_view.person.clone(),
community,
reason: data.reason.clone(),
removed: true,
},
&context,
)
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -1,53 +1,51 @@
use crate::ban_nonlocal_user_from_local_communities; use actix_web::web::{Data, Json};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData}, request::delete_image_from_pictrs,
site::PurgePerson, site::PurgePerson,
utils::{is_admin, purge_user_account}, utils::is_admin,
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
image_upload::ImageUpload,
moderator::{AdminPurgePerson, AdminPurgePersonForm}, moderator::{AdminPurgePerson, AdminPurgePersonForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
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::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_person( pub async fn purge_person(
data: Json<PurgePerson>, data: Json<PurgePerson>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let person = Person::read(&mut context.pool(), data.person_id) // Read the person to get their images
.await? let person_id = data.person_id;
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await {
&local_user_view, let pictrs_uploads =
&person, ImageUpload::get_all_by_local_user_id(&mut context.pool(), &local_user.local_user.id).await?;
true,
&data.reason, for upload in pictrs_uploads {
&Some(true), delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context)
&None, .await
&context, .ok();
) }
.await?; }
// Clear profile data. // Clear profile data.
purge_user_account(data.person_id, &context).await?; Person::delete_account(&mut context.pool(), person_id).await?;
// Keep person record, but mark as banned to prevent login or refetching from home instance. // Keep person record, but mark as banned to prevent login or refetching from home instance.
let person = Person::update( Person::update(
&mut context.pool(), &mut context.pool(),
data.person_id, person_id,
&PersonUpdateForm { &PersonUpdateForm {
banned: Some(true), banned: Some(true),
..Default::default() ..Default::default()
@ -60,20 +58,8 @@ pub async fn purge_person(
admin_person_id: local_user_view.person.id, admin_person_id: local_user_view.person.id,
reason: data.reason.clone(), reason: data.reason.clone(),
}; };
AdminPurgePerson::create(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity( AdminPurgePerson::create(&mut context.pool(), &form).await?;
SendActivityData::BanFromSite {
moderator: local_user_view.person,
banned_user: person,
reason: data.reason.clone(),
remove_data: Some(true),
ban: true,
expires: None,
},
&context,
)
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -1,9 +1,7 @@
use activitypub_federation::config::Data; use actix_web::web::{Data, Json};
use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
request::purge_image_from_pictrs, request::purge_image_from_pictrs,
send_activity::{ActivityChannel, SendActivityData},
site::PurgePost, site::PurgePost,
utils::is_admin, utils::is_admin,
SuccessResponse, SuccessResponse,
@ -16,51 +14,43 @@ 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::LemmyError;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_post( pub async fn purge_post(
data: Json<PurgePost>, data: Json<PurgePost>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let post_id = data.post_id;
// 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(), post_id).await?;
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Purge image // Purge image
if let Some(url) = &post.url { if let Some(url) = post.url {
purge_image_from_pictrs(url, &context).await.ok(); purge_image_from_pictrs(&url, &context).await.ok();
} }
// Purge thumbnail // Purge thumbnail
if let Some(thumbnail_url) = &post.thumbnail_url { if let Some(thumbnail_url) = post.thumbnail_url {
purge_image_from_pictrs(thumbnail_url, &context).await.ok(); purge_image_from_pictrs(&thumbnail_url, &context).await.ok();
} }
Post::delete(&mut context.pool(), data.post_id).await?; let community_id = post.community_id;
Post::delete(&mut context.pool(), post_id).await?;
// Mod tables // Mod tables
let form = AdminPurgePostForm { let form = AdminPurgePostForm {
admin_person_id: local_user_view.person.id, admin_person_id: local_user_view.person.id,
reason: data.reason.clone(), reason: data.reason.clone(),
community_id: post.community_id, community_id,
}; };
AdminPurgePost::create(&mut context.pool(), &form).await?;
ActivityChannel::submit_activity( AdminPurgePost::create(&mut context.pool(), &form).await?;
SendActivityData::RemovePost {
post,
moderator: local_user_view.person.clone(),
reason: data.reason.clone(),
removed: true,
},
&context,
)
.await?;
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))
} }

View file

@ -10,23 +10,23 @@ use lemmy_db_schema::{
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm}, registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::diesel_string_update, utils::diesel_option_overwrite,
}; };
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType}; use lemmy_utils::error::LemmyError;
pub async fn approve_registration_application( pub async fn approve_registration_application(
data: Json<ApproveRegistrationApplication>, data: Json<ApproveRegistrationApplication>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<RegistrationApplicationResponse>> { ) -> Result<Json<RegistrationApplicationResponse>, LemmyError> {
let app_id = data.id; let app_id = data.id;
// Only let admins do this // Only let admins do this
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Update the registration with reason, admin_id // Update the registration with reason, admin_id
let deny_reason = diesel_string_update(data.deny_reason.as_deref()); let deny_reason = diesel_option_overwrite(data.deny_reason.clone());
let app_form = RegistrationApplicationUpdateForm { let app_form = RegistrationApplicationUpdateForm {
admin_id: Some(Some(local_user_view.person.id)), admin_id: Some(Some(local_user_view.person.id)),
deny_reason, deny_reason,
@ -45,9 +45,8 @@ pub async fn approve_registration_application(
LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?; LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).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() {
send_application_approved_email(&approved_local_user_view, context.settings()).await?; send_application_approved_email(&approved_local_user_view, context.settings()).await?;
@ -55,9 +54,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

@ -9,14 +9,14 @@ use lemmy_db_views::{
registration_application_view::RegistrationApplicationQuery, registration_application_view::RegistrationApplicationQuery,
structs::LocalUserView, structs::LocalUserView,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
/// Lists registration applications, filterable by undenied only. /// Lists registration applications, filterable by undenied only.
pub async fn list_registration_applications( pub async fn list_registration_applications(
data: Query<ListRegistrationApplications>, data: Query<ListRegistrationApplications>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListRegistrationApplicationsResponse>> { ) -> Result<Json<ListRegistrationApplicationsResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin // Make sure user is an admin

View file

@ -6,12 +6,12 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyError;
pub async fn get_unread_registration_application_count( pub async fn get_unread_registration_application_count(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<GetUnreadRegistrationApplicationCountResponse>> { ) -> Result<Json<GetUnreadRegistrationApplicationCountResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
// Only let admins do this // Only let admins do this

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