mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-26 06:11:26 +00:00
Merge remote-tracking branch 'origin/main' into enable_private_messages_setting
This commit is contained in:
commit
0204b35324
601 changed files with 34972 additions and 19227 deletions
|
@ -1,2 +0,0 @@
|
|||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -1,3 +1,3 @@
|
|||
* @Nutomic @dessalines @phiresky
|
||||
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917
|
||||
crates/apub/ @Nutomic
|
||||
migrations/ @dessalines @phiresky
|
||||
migrations/ @dessalines @phiresky @dullbananas
|
||||
|
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
|
@ -20,6 +20,8 @@ body:
|
|||
required: true
|
||||
- label: Is this only a single bug? Do not put multiple bugs in one issue.
|
||||
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.
|
||||
required: true
|
||||
- type: textarea
|
||||
|
|
2
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
2
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
|
@ -20,6 +20,8 @@ body:
|
|||
required: true
|
||||
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
|
||||
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
|
||||
id: problem
|
||||
attributes:
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,6 +20,7 @@ query_testing/**/reports/*.json
|
|||
api_tests/node_modules
|
||||
api_tests/.yalc
|
||||
api_tests/yalc.lock
|
||||
api_tests/pict-rs
|
||||
|
||||
# pictrs data
|
||||
pictrs/
|
||||
|
|
|
@ -3,3 +3,5 @@ edition = "2021"
|
|||
imports_layout = "HorizontalVertical"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "One"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
|
|
278
.woodpecker.yml
278
.woodpecker.yml
|
@ -2,32 +2,34 @@
|
|||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
||||
|
||||
variables:
|
||||
- &rust_image "rust:1.72.1"
|
||||
- &rust_image "rust:1.81"
|
||||
- &rust_nightly_image "rustlang/rust:nightly"
|
||||
- &install_pnpm "corepack enable pnpm"
|
||||
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
|
||||
- install_diesel_cli: &install_diesel_cli
|
||||
- apt-get update && apt-get install -y postgresql-client
|
||||
- cargo install diesel_cli --no-default-features --features postgres
|
||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||
- &slow_check_paths
|
||||
- path:
|
||||
- event: pull_request
|
||||
path:
|
||||
include: [
|
||||
# rust source code
|
||||
- "**/*.rs"
|
||||
- "**/Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
"crates/**",
|
||||
"src/**",
|
||||
"**/Cargo.toml",
|
||||
"Cargo.lock",
|
||||
# database migrations
|
||||
- "migrations/**"
|
||||
"migrations/**",
|
||||
# typescript tests
|
||||
- "api_tests/**"
|
||||
"api_tests/**",
|
||||
# config files and scripts used by ci
|
||||
- ".woodpecker.yml"
|
||||
- ".rustfmt.toml"
|
||||
- "scripts/update_config_defaults.sh"
|
||||
- "diesel.toml"
|
||||
- ".gitmodules"
|
||||
|
||||
# Broken for cron jobs currently, see
|
||||
# https://github.com/woodpecker-ci/woodpecker/issues/1716
|
||||
# clone:
|
||||
# git:
|
||||
# image: woodpeckerci/plugin-git
|
||||
# settings:
|
||||
# recursive: true
|
||||
# submodule_update_remote: true
|
||||
".woodpecker.yml",
|
||||
".rustfmt.toml",
|
||||
"scripts/update_config_defaults.sh",
|
||||
"diesel.toml",
|
||||
".gitmodules",
|
||||
]
|
||||
|
||||
steps:
|
||||
prepare_repo:
|
||||
|
@ -36,64 +38,64 @@ steps:
|
|||
- apk add git
|
||||
- git submodule init
|
||||
- git submodule update
|
||||
when:
|
||||
- event: [pull_request, tag]
|
||||
|
||||
prettier_check:
|
||||
group: format
|
||||
image: tmknom/prettier:3.0.0
|
||||
image: tmknom/prettier:3.2.5
|
||||
commands:
|
||||
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations'
|
||||
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
toml_fmt:
|
||||
group: format
|
||||
image: tamasfe/taplo:0.8.1
|
||||
image: tamasfe/taplo:0.9.3
|
||||
commands:
|
||||
- taplo format --check
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
sql_fmt:
|
||||
group: format
|
||||
image: backplane/pgformatter:latest
|
||||
image: backplane/pgformatter
|
||||
commands:
|
||||
- ./scripts/sql_format_check.sh
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
cargo_fmt:
|
||||
group: format
|
||||
image: rustlang/rust:nightly
|
||||
image: *rust_nightly_image
|
||||
environment:
|
||||
# store cargo data in repo folder so that it gets cached between steps
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
# need make existing toolchain available
|
||||
- rustup component add rustfmt
|
||||
- cargo +nightly fmt -- --check
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
restore-cache:
|
||||
image: meltwater/drone-cache:v1
|
||||
pull: true
|
||||
settings:
|
||||
restore: true
|
||||
endpoint:
|
||||
from_secret: MINIO_ENDPOINT
|
||||
access-key:
|
||||
from_secret: MINIO_WRITE_USER
|
||||
secret-key:
|
||||
from_secret: MINIO_WRITE_PASSWORD
|
||||
bucket:
|
||||
from_secret: MINIO_BUCKET
|
||||
region: us-east-1
|
||||
cache_key: "rust-cache"
|
||||
path-style: true
|
||||
mount:
|
||||
- ".cargo"
|
||||
- "target"
|
||||
- "api_tests/node_modules"
|
||||
secrets:
|
||||
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
|
||||
when: *slow_check_paths
|
||||
cargo_shear:
|
||||
image: *rust_nightly_image
|
||||
commands:
|
||||
- *install_binstall
|
||||
- cargo binstall -y cargo-shear
|
||||
- cargo shear
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
ignored_files:
|
||||
image: alpine:3
|
||||
commands:
|
||||
- apk add git
|
||||
- IGNORED=$(git ls-files --cached -i --exclude-standard)
|
||||
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
# make sure api builds with default features (used by other crates relying on lemmy api)
|
||||
check_api_common_default_features:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- cargo check --package lemmy_api_common
|
||||
when: *slow_check_paths
|
||||
|
@ -101,7 +103,7 @@ steps:
|
|||
lemmy_api_common_doesnt_depend_on_diesel:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
|
||||
when: *slow_check_paths
|
||||
|
@ -109,7 +111,7 @@ steps:
|
|||
lemmy_api_common_works_with_wasm:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- "rustup target add wasm32-unknown-unknown"
|
||||
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
|
||||
|
@ -118,7 +120,7 @@ steps:
|
|||
check_defaults_hjson_updated:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- export LEMMY_CONFIG_LOCATION=./config/config.hjson
|
||||
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
|
||||
|
@ -126,138 +128,160 @@ steps:
|
|||
when: *slow_check_paths
|
||||
|
||||
check_diesel_schema:
|
||||
image: willsquire/diesel-cli
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
commands:
|
||||
- <<: *install_diesel_cli
|
||||
- diesel migration run
|
||||
- diesel print-schema --config-file=diesel.toml > tmp.schema
|
||||
- diff tmp.schema crates/db_schema/src/schema.rs
|
||||
when: *slow_check_paths
|
||||
|
||||
check_diesel_migration_revertable:
|
||||
image: willsquire/diesel-cli
|
||||
check_db_perf_tool:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- diesel migration run
|
||||
- diesel migration redo
|
||||
# same as scripts/db_perf.sh but without creating a new database server
|
||||
- export LEMMY_CONFIG_LOCATION=config/config.hjson
|
||||
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_clippy:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
# when adding new clippy lints, make sure to also add them in scripts/lint.sh
|
||||
- rustup component add clippy
|
||||
- cargo clippy --workspace --tests --all-targets --features console --
|
||||
-D warnings -D deprecated -D clippy::perf -D clippy::complexity
|
||||
-D clippy::style -D clippy::correctness -D clippy::suspicious
|
||||
-D clippy::dbg_macro -D clippy::inefficient_to_string
|
||||
-D clippy::items-after-statements -D clippy::implicit_clone
|
||||
-D clippy::cast_lossless -D clippy::manual_string_new
|
||||
-D clippy::redundant_closure_for_method_calls
|
||||
-D clippy::unused_self
|
||||
-A clippy::uninlined_format_args
|
||||
-D clippy::get_first
|
||||
-D clippy::explicit_into_iter_loop
|
||||
-D clippy::explicit_iter_loop
|
||||
-D clippy::needless_collect
|
||||
-D clippy::unwrap_used
|
||||
-D clippy::indexing_slicing
|
||||
- cargo clippy --workspace --tests --all-targets -- -D warnings
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_build:
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
commands:
|
||||
- cargo build
|
||||
- mv target/debug/lemmy_server target/lemmy_server
|
||||
when: *slow_check_paths
|
||||
|
||||
cargo_test:
|
||||
group: tests
|
||||
image: *rust_image
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo
|
||||
CARGO_HOME: .cargo_home
|
||||
LEMMY_TEST_FAST_FEDERATION: "1"
|
||||
commands:
|
||||
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
|
||||
- cargo test --workspace --no-fail-fast
|
||||
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
|
||||
- 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:
|
||||
group: tests
|
||||
image: node:20-bookworm-slim
|
||||
image: node:22-bookworm-slim
|
||||
environment:
|
||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
|
||||
DO_WRITE_HOSTS_FILE: "1"
|
||||
commands:
|
||||
- apt update && apt install -y bash curl postgresql-client
|
||||
- *install_pnpm
|
||||
- apt-get update && apt-get install -y bash curl postgresql-client
|
||||
- bash api_tests/prepare-drone-federation-test.sh
|
||||
- cd api_tests/
|
||||
- yarn
|
||||
- yarn api-test
|
||||
- pnpm i
|
||||
- pnpm api-test
|
||||
when: *slow_check_paths
|
||||
|
||||
rebuild-cache:
|
||||
image: meltwater/drone-cache:v1
|
||||
pull: true
|
||||
settings:
|
||||
rebuild: true
|
||||
endpoint:
|
||||
from_secret: MINIO_ENDPOINT
|
||||
access-key:
|
||||
from_secret: MINIO_WRITE_USER
|
||||
secret-key:
|
||||
from_secret: MINIO_WRITE_PASSWORD
|
||||
bucket:
|
||||
from_secret: MINIO_BUCKET
|
||||
cache_key: "rust-cache"
|
||||
region: us-east-1
|
||||
path-style: true
|
||||
mount:
|
||||
- ".cargo"
|
||||
- "target"
|
||||
- "api_tests/node_modules"
|
||||
secrets:
|
||||
[MINIO_ENDPOINT, MINIO_WRITE_USER, MINIO_WRITE_PASSWORD, MINIO_BUCKET]
|
||||
federation_tests_server_output:
|
||||
image: alpine:3
|
||||
commands:
|
||||
# `|| true` prevents this step from appearing to fail if the server output files don't exist
|
||||
- cat target/log/lemmy_*.out || true
|
||||
- "# If you can't see all output, then use the download button"
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: pull_request
|
||||
status: failure
|
||||
|
||||
publish_release_docker:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy
|
||||
dockerfile: docker/Dockerfile
|
||||
# TODO fix arm build: see: https://woodpecker.join-lemmy.org/repos/129/pipeline/2888/20
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
platforms: linux/amd64, linux/arm64
|
||||
build_args:
|
||||
- RUST_RELEASE_MODE=release
|
||||
tag: ${CI_COMMIT_TAG}
|
||||
when:
|
||||
event: tag
|
||||
- event: tag
|
||||
|
||||
nightly_build:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy
|
||||
dockerfile: docker/Dockerfile
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build_args:
|
||||
- RUST_RELEASE_MODE=release
|
||||
tag: dev
|
||||
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:
|
||||
image: alpine:3
|
||||
|
@ -265,7 +289,8 @@ steps:
|
|||
- apk add curl
|
||||
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
|
||||
when:
|
||||
status: [failure]
|
||||
- event: [pull_request, tag]
|
||||
status: failure
|
||||
|
||||
notify_on_tag_deploy:
|
||||
image: alpine:3
|
||||
|
@ -273,11 +298,12 @@ steps:
|
|||
- apk add curl
|
||||
- "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
|
||||
when:
|
||||
event: tag
|
||||
- event: tag
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15.2-alpine
|
||||
# 15-alpine image necessary because of diesel tests
|
||||
image: pgautoupgrade/pgautoupgrade:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: lemmy
|
||||
POSTGRES_PASSWORD: password
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
See [here](https://join-lemmy.org/docs/en/contributors/01-overview.html) for contributing Instructions.
|
4898
Cargo.lock
generated
4898
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
187
Cargo.toml
187
Cargo.toml
|
@ -1,5 +1,5 @@
|
|||
[workspace.package]
|
||||
version = "0.19.0-rc.3"
|
||||
version = "0.19.6-beta.7"
|
||||
edition = "2021"
|
||||
description = "A link aggregator for the fediverse"
|
||||
license = "AGPL-3.0"
|
||||
|
@ -16,15 +16,20 @@ license.workspace = true
|
|||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[profile.release]
|
||||
debug = 0
|
||||
lto = "thin"
|
||||
lto = "fat"
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
opt-level = "z" # Optimize for size.
|
||||
opt-level = 3 # Optimize for speed, not size.
|
||||
codegen-units = 1 # Reduce parallel code generation.
|
||||
|
||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
||||
# out temporarily, but make sure to leave this in the main branch.
|
||||
|
@ -32,16 +37,7 @@ opt-level = "z" # Optimize for size.
|
|||
debug = 0
|
||||
|
||||
[features]
|
||||
embed-pictrs = ["pict-rs"]
|
||||
console = [
|
||||
"console-subscriber",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"tracing-opentelemetry",
|
||||
"reqwest-tracing/opentelemetry_0_16",
|
||||
]
|
||||
json-log = ["tracing-subscriber/json"]
|
||||
prometheus-metrics = ["prometheus", "actix-web-prom"]
|
||||
default = []
|
||||
|
||||
[workspace]
|
||||
|
@ -51,6 +47,7 @@ members = [
|
|||
"crates/api_common",
|
||||
"crates/apub",
|
||||
"crates/utils",
|
||||
"crates/db_perf",
|
||||
"crates/db_schema",
|
||||
"crates/db_views",
|
||||
"crates/db_views_actor",
|
||||
|
@ -59,77 +56,108 @@ members = [
|
|||
"crates/federate",
|
||||
]
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
complexity = { level = "deny", priority = -1 }
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
dbg_macro = "deny"
|
||||
explicit_into_iter_loop = "deny"
|
||||
explicit_iter_loop = "deny"
|
||||
get_first = "deny"
|
||||
implicit_clone = "deny"
|
||||
indexing_slicing = "deny"
|
||||
inefficient_to_string = "deny"
|
||||
items-after-statements = "deny"
|
||||
manual_string_new = "deny"
|
||||
needless_collect = "deny"
|
||||
perf = { level = "deny", priority = -1 }
|
||||
redundant_closure_for_method_calls = "deny"
|
||||
style = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
uninlined_format_args = "allow"
|
||||
unused_self = "deny"
|
||||
unwrap_used = "deny"
|
||||
unimplemented = "deny"
|
||||
|
||||
[workspace.dependencies]
|
||||
lemmy_api = { version = "=0.19.0-rc.3", path = "./crates/api" }
|
||||
lemmy_api_crud = { version = "=0.19.0-rc.3", path = "./crates/api_crud" }
|
||||
lemmy_apub = { version = "=0.19.0-rc.3", path = "./crates/apub" }
|
||||
lemmy_utils = { version = "=0.19.0-rc.3", path = "./crates/utils" }
|
||||
lemmy_db_schema = { version = "=0.19.0-rc.3", path = "./crates/db_schema" }
|
||||
lemmy_api_common = { version = "=0.19.0-rc.3", path = "./crates/api_common" }
|
||||
lemmy_routes = { version = "=0.19.0-rc.3", path = "./crates/routes" }
|
||||
lemmy_db_views = { version = "=0.19.0-rc.3", path = "./crates/db_views" }
|
||||
lemmy_db_views_actor = { version = "=0.19.0-rc.3", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.0-rc.3", path = "./crates/db_views_moderator" }
|
||||
activitypub_federation = { version = "0.5.0-beta.3", default-features = false, features = [
|
||||
lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }
|
||||
lemmy_api_crud = { version = "=0.19.6-beta.7", path = "./crates/api_crud" }
|
||||
lemmy_apub = { version = "=0.19.6-beta.7", path = "./crates/apub" }
|
||||
lemmy_utils = { version = "=0.19.6-beta.7", path = "./crates/utils", default-features = false }
|
||||
lemmy_db_schema = { version = "=0.19.6-beta.7", path = "./crates/db_schema" }
|
||||
lemmy_api_common = { version = "=0.19.6-beta.7", path = "./crates/api_common" }
|
||||
lemmy_routes = { version = "=0.19.6-beta.7", path = "./crates/routes" }
|
||||
lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
|
||||
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
|
||||
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
|
||||
activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
|
||||
"actix-web",
|
||||
] }
|
||||
diesel = "2.1.0"
|
||||
diesel = "2.1.6"
|
||||
diesel_migrations = "2.1.0"
|
||||
diesel-async = "0.3.1"
|
||||
serde = { version = "1.0.167", features = ["derive"] }
|
||||
serde_with = "3.0.0"
|
||||
actix-web = { version = "4.3.1", default-features = false, features = [
|
||||
diesel-async = "0.4.1"
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
serde_with = "3.9.0"
|
||||
actix-web = { version = "4.9.0", default-features = false, features = [
|
||||
"macros",
|
||||
"rustls",
|
||||
"rustls-0_23",
|
||||
"compress-brotli",
|
||||
"compress-gzip",
|
||||
"compress-zstd",
|
||||
"cookies",
|
||||
] }
|
||||
tracing = "0.1.37"
|
||||
tracing-actix-web = { version = "0.7.5", default-features = false }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
url = { version = "2.4.0", features = ["serde"] }
|
||||
reqwest = { version = "0.11.18", features = ["json", "blocking", "gzip"] }
|
||||
reqwest-middleware = "0.2.2"
|
||||
reqwest-tracing = "0.4.5"
|
||||
tracing = "0.1.40"
|
||||
tracing-actix-web = { version = "0.7.10", default-features = false }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
reqwest = { version = "0.12.7", default-features = false, features = [
|
||||
"json",
|
||||
"blocking",
|
||||
"gzip",
|
||||
"rustls-tls",
|
||||
] }
|
||||
reqwest-middleware = "0.3.3"
|
||||
reqwest-tracing = "0.5.3"
|
||||
clokwerk = "0.4.0"
|
||||
doku = { version = "0.21.1", features = ["url-2"] }
|
||||
bcrypt = "0.15.0"
|
||||
chrono = { version = "0.4.26", features = ["serde"], default-features = false }
|
||||
serde_json = { version = "1.0.100", features = ["preserve_order"] }
|
||||
base64 = "0.21.2"
|
||||
uuid = { version = "1.4.0", features = ["serde", "v4"] }
|
||||
async-trait = "0.1.71"
|
||||
bcrypt = "0.15.1"
|
||||
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
||||
serde_json = { version = "1.0.121", features = ["preserve_order"] }
|
||||
base64 = "0.22.1"
|
||||
uuid = { version = "1.10.0", features = ["serde", "v4"] }
|
||||
async-trait = "0.1.81"
|
||||
captcha = "0.0.9"
|
||||
anyhow = { version = "1.0.71", features = [
|
||||
anyhow = { version = "1.0.86", features = [
|
||||
"backtrace",
|
||||
] } # backtrace is on by default on nightly, but not stable rust
|
||||
diesel_ltree = "0.3.0"
|
||||
typed-builder = "0.15.0"
|
||||
serial_test = "2.0.0"
|
||||
tokio = { version = "1.29.1", features = ["full"] }
|
||||
regex = "1.9.0"
|
||||
once_cell = "1.18.0"
|
||||
diesel-derive-newtype = "2.1.0"
|
||||
diesel_ltree = "0.3.1"
|
||||
serial_test = "3.1.1"
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
regex = "1.10.5"
|
||||
diesel-derive-newtype = "2.1.2"
|
||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||
strum = "0.25.0"
|
||||
strum_macros = "0.25.1"
|
||||
itertools = "0.11.0"
|
||||
futures = "0.3.28"
|
||||
http = "0.2.9"
|
||||
percent-encoding = "2.3.0"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
itertools = "0.13.0"
|
||||
futures = "0.3.30"
|
||||
http = "1.1"
|
||||
rosetta-i18n = "0.1.3"
|
||||
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
|
||||
tracing-opentelemetry = { version = "0.19.0" }
|
||||
ts-rs = { version = "7.0.0", features = ["serde-compat", "chrono-impl"] }
|
||||
rustls = { version = "0.21.3", features = ["dangerous_configuration"] }
|
||||
futures-util = "0.3.28"
|
||||
tokio-postgres = "0.7.8"
|
||||
tokio-postgres-rustls = "0.10.0"
|
||||
enum-map = "2.6"
|
||||
ts-rs = { version = "7.1.1", features = [
|
||||
"serde-compat",
|
||||
"chrono-impl",
|
||||
"no-serde-warnings",
|
||||
] }
|
||||
rustls = { version = "0.23.12", features = ["ring"] }
|
||||
futures-util = "0.3.30"
|
||||
tokio-postgres = "0.7.11"
|
||||
tokio-postgres-rustls = "0.12.0"
|
||||
urlencoding = "2.1.3"
|
||||
enum-map = "2.7"
|
||||
moka = { version = "0.12.8", features = ["future"] }
|
||||
i-love-jesus = { version = "0.1.0" }
|
||||
clap = { version = "4.5.13", features = ["derive", "env"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
derive-new = "0.7.0"
|
||||
|
||||
[dependencies]
|
||||
lemmy_api = { workspace = true }
|
||||
|
@ -139,38 +167,29 @@ lemmy_utils = { workspace = true }
|
|||
lemmy_db_schema = { workspace = true }
|
||||
lemmy_api_common = { workspace = true }
|
||||
lemmy_routes = { workspace = true }
|
||||
lemmy_federate = { version = "0.19.0-rc.3", path = "crates/federate" }
|
||||
lemmy_federate = { workspace = true }
|
||||
activitypub_federation = { workspace = true }
|
||||
diesel = { workspace = true }
|
||||
diesel-async = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-actix-web = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
tracing-log = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
reqwest-tracing = { workspace = true }
|
||||
clokwerk = { workspace = true }
|
||||
doku = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, optional = true }
|
||||
console-subscriber = { version = "0.1.10", optional = true }
|
||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
||||
pict-rs = { version = "0.4.0-rc.12", optional = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.6.4"
|
||||
rustls = { workspace = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.7.0"
|
||||
futures-util = { workspace = true }
|
||||
tokio-postgres = { workspace = true }
|
||||
tokio-postgres-rustls = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
prometheus = { version = "0.13.3", features = ["process"], optional = true }
|
||||
actix-web-prom = { version = "0.6.0", optional = true }
|
||||
prometheus = { version = "0.13.4", features = ["process"] }
|
||||
serial_test = { workspace = true }
|
||||
clap = { version = "4.3.19", features = ["derive"] }
|
||||
actix-web-httpauth = "0.8.1"
|
||||
clap = { workspace = true }
|
||||
actix-web-prom = "0.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
|
19
README.md
19
README.md
|
@ -7,7 +7,7 @@
|
|||
[![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)
|
||||
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
|
||||
[![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)
|
||||
<a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -48,8 +48,8 @@
|
|||
## About The Project
|
||||
|
||||
| Desktop | Mobile |
|
||||
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
|
||||
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
|
||||
|
||||
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
|
||||
|
||||
|
@ -107,7 +107,6 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
|||
- NSFW post / community support.
|
||||
- High performance.
|
||||
- Server is written in rust.
|
||||
- Front end is `~80kB` gzipped.
|
||||
- Supports arm64 / Raspberry Pi.
|
||||
|
||||
## Installation
|
||||
|
@ -122,6 +121,8 @@ 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 made possible by a generous grant from the [NLnet foundation](https://nlnet.nl/).
|
||||
|
||||
- [Support on Liberapay](https://liberapay.com/Lemmy).
|
||||
- [Support on Patreon](https://www.patreon.com/dessalines).
|
||||
- [Support on OpenCollective](https://opencollective.com/lemmy).
|
||||
|
@ -132,21 +133,25 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
|||
- bitcoin: `1Hefs7miXS5ff5Ck5xvmjKjXf5242KzRtK`
|
||||
- ethereum: `0x400c96c96acbC6E7B3B43B1dc1BB446540a88A01`
|
||||
- monero: `41taVyY6e1xApqKyMVDRVxJ76sPkfZhALLTjRvVKpaAh2pBd4wv9RgYj1tSPrx8wc6iE1uWUfjtQdTmTy2FGMeChGVKPQuV`
|
||||
- cardano: `addr1q858t89l2ym6xmrugjs0af9cslfwvnvsh2xxp6x4dcez7pf5tushkp4wl7zxfhm2djp6gq60dk4cmc7seaza5p3slx0sakjutm`
|
||||
|
||||
## Contributing
|
||||
|
||||
Read the following documentation to setup the development environment and start coding:
|
||||
|
||||
- [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.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)
|
||||
|
||||
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
|
||||
|
||||
- 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).
|
||||
|
||||
## Contact
|
||||
## Community
|
||||
|
||||
- [Mastodon](https://mastodon.social/@LemmyDev)
|
||||
- [Matrix Space](https://matrix.to/#/#lemmy-space:matrix.org)
|
||||
- [Lemmy Forum](https://lemmy.ml/c/lemmy)
|
||||
- [Lemmy Support Forum](https://lemmy.ml/c/lemmy_support)
|
||||
|
||||
## Code Mirrors
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Message contact at join-lemmy.org for any security-related issues.
|
||||
Use [Github's security advisory issue system](https://github.com/LemmyNet/lemmy/security/advisories/new).
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
1
api_tests/.npmrc
Normal file
1
api_tests/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-manager-strict=false
|
56
api_tests/eslint.config.mjs
Normal file
56
api_tests/eslint.config.mjs
Normal file
|
@ -0,0 +1,56 @@
|
|||
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,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -6,22 +6,31 @@
|
|||
"repository": "https://github.com/LemmyNet/lemmy",
|
||||
"author": "Dessalines",
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
|
||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
|
||||
"fix": "prettier --write src && eslint --fix src",
|
||||
"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"
|
||||
"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-follow": "jest -i follow.spec.ts",
|
||||
"api-test-comment": "jest -i comment.spec.ts",
|
||||
"api-test-post": "jest -i post.spec.ts",
|
||||
"api-test-user": "jest -i user.spec.ts",
|
||||
"api-test-community": "jest -i community.spec.ts",
|
||||
"api-test-private-message": "jest -i private_message.spec.ts",
|
||||
"api-test-image": "jest -i image.spec.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/node": "^20.8.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^22.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
||||
"@typescript-eslint/parser": "^8.1.0",
|
||||
"eslint": "^9.9.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.5.0",
|
||||
"lemmy-js-client": "0.19.0-rc.12",
|
||||
"prettier": "^3.0.0",
|
||||
"lemmy-js-client": "0.20.0-alpha.11",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.1.0"
|
||||
}
|
||||
}
|
||||
|
|
3444
api_tests/pnpm-lock.yaml
Normal file
3444
api_tests/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,11 +3,28 @@
|
|||
# it is expected that this script is called by run-federation-test.sh script.
|
||||
set -e
|
||||
|
||||
if [ -z "$LEMMY_LOG_LEVEL" ];
|
||||
then
|
||||
LEMMY_LOG_LEVEL=info
|
||||
fi
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
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 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 LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
|
||||
|
||||
# pictrs setup
|
||||
if [ ! -f "api_tests/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
|
||||
chmod +x api_tests/pict-rs
|
||||
fi
|
||||
./api_tests/pict-rs \
|
||||
run -a 0.0.0.0:8080 \
|
||||
--danger-dummy-mode \
|
||||
--api-key "my-pictrs-key" \
|
||||
filesystem -p /tmp/pictrs/files \
|
||||
sled -p /tmp/pictrs/sled-repo 2>&1 &
|
||||
|
||||
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
|
||||
echo "DB URL: ${LEMMY_DATABASE_URL} INSTANCE: $INSTANCE"
|
||||
psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
|
||||
|
@ -34,32 +51,35 @@ fi
|
|||
|
||||
echo "$PWD"
|
||||
|
||||
LOG_DIR=target/log
|
||||
mkdir -p $LOG_DIR
|
||||
|
||||
echo "start alpha"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
|
||||
target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
|
||||
target/lemmy_server >$LOG_DIR/lemmy_alpha.out 2>&1 &
|
||||
|
||||
echo "start beta"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
|
||||
target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
|
||||
target/lemmy_server >$LOG_DIR/lemmy_beta.out 2>&1 &
|
||||
|
||||
echo "start gamma"
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
|
||||
target/lemmy_server >/tmp/lemmy_gamma.out 2>&1 &
|
||||
target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 &
|
||||
|
||||
echo "start delta"
|
||||
# An instance with only an allowlist for beta
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
|
||||
target/lemmy_server >/tmp/lemmy_delta.out 2>&1 &
|
||||
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 &
|
||||
|
||||
echo "start epsilon"
|
||||
# An instance who has a blocklist, with lemmy-alpha blocked
|
||||
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
|
||||
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
|
||||
target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
|
||||
target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 &
|
||||
|
||||
echo "wait for all instances to start"
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done
|
||||
|
|
|
@ -10,10 +10,12 @@ killall -s1 lemmy_server || true
|
|||
./api_tests/prepare-drone-federation-test.sh
|
||||
popd
|
||||
|
||||
yarn
|
||||
yarn api-test || true
|
||||
pnpm i
|
||||
pnpm api-test || true
|
||||
|
||||
killall -s1 lemmy_server || true
|
||||
killall -s1 pict-rs || true
|
||||
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
|
||||
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"
|
||||
done
|
||||
rm -r /tmp/pictrs
|
||||
|
|
|
@ -35,17 +35,17 @@ import {
|
|||
waitForPost,
|
||||
alphaUrl,
|
||||
followCommunity,
|
||||
blockCommunity,
|
||||
delay,
|
||||
saveUserSettings,
|
||||
} from "./shared";
|
||||
import { CommentView } from "lemmy-js-client/dist/types/CommentView";
|
||||
import { CommunityView } from "lemmy-js-client";
|
||||
import { LemmyHttp } from "lemmy-js-client";
|
||||
import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client";
|
||||
|
||||
let betaCommunity: CommunityView | undefined;
|
||||
let postOnAlphaRes: PostResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
await unfollows();
|
||||
await Promise.all([followBeta(alpha), followBeta(gamma)]);
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
if (betaCommunity) {
|
||||
|
@ -53,9 +53,7 @@ beforeAll(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await unfollows();
|
||||
});
|
||||
afterAll(unfollows);
|
||||
|
||||
function assertCommentFederation(
|
||||
commentOne?: CommentView,
|
||||
|
@ -93,7 +91,9 @@ test("Create a comment", async () => {
|
|||
});
|
||||
|
||||
test("Create a comment in a non-existent post", async () => {
|
||||
await expect(createComment(alpha, -1)).rejects.toBe("couldnt_find_post");
|
||||
await expect(createComment(alpha, -1)).rejects.toStrictEqual(
|
||||
Error("not_found"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Update a comment", async () => {
|
||||
|
@ -126,8 +126,9 @@ test("Update 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)
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
let commentRes = await createComment(alpha, post.post_view.post.id);
|
||||
|
||||
// Find the comment on beta (home of community)
|
||||
let betaComment = (
|
||||
|
@ -142,7 +143,7 @@ test("Delete a comment", async () => {
|
|||
await waitUntil(
|
||||
() =>
|
||||
resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
||||
r => r !== "couldnt_find_object",
|
||||
r => r.message !== "not_found",
|
||||
)
|
||||
).comment;
|
||||
if (!gammaComment) {
|
||||
|
@ -155,17 +156,18 @@ test("Delete a comment", async () => {
|
|||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
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 deleted on beta
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e === "couldnt_find_object",
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === true,
|
||||
);
|
||||
|
||||
// Make sure that comment is undefined on gamma after delete
|
||||
// Make sure that comment is deleted on gamma after delete
|
||||
await waitUntil(
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e === "couldnt_find_object",
|
||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === true,
|
||||
);
|
||||
|
||||
// Test undeleting the comment
|
||||
|
@ -179,11 +181,10 @@ test("Delete a comment", async () => {
|
|||
// Make sure that comment is undeleted on beta
|
||||
let betaComment2 = (
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
|
||||
e => e !== "couldnt_find_object",
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.comment.deleted === false,
|
||||
)
|
||||
).comment;
|
||||
expect(betaComment2?.comment.deleted).toBe(false);
|
||||
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
|
||||
});
|
||||
|
||||
|
@ -227,10 +228,7 @@ test.skip("Remove a comment from admin and community on the same instance", asyn
|
|||
});
|
||||
|
||||
test("Remove a comment from admin and community on different instance", async () => {
|
||||
let alpha_user = await registerUser(alpha);
|
||||
let newAlphaApi = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: `Bearer ${alpha_user.jwt ?? ""}` },
|
||||
});
|
||||
let newAlphaApi = await registerUser(alpha, alphaUrl);
|
||||
|
||||
// New alpha user creates a community, post, and comment.
|
||||
let newCommunity = await createCommunity(newAlphaApi);
|
||||
|
@ -256,6 +254,16 @@ test("Remove a comment from admin and community on different instance", async ()
|
|||
betaComment.comment.id,
|
||||
);
|
||||
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
|
||||
let refetchedPostComments = await getComments(
|
||||
|
@ -345,17 +353,26 @@ test("Federated comment like", async () => {
|
|||
test("Reply to a comment from another instance, get notification", async () => {
|
||||
await alpha.markAllAsRead();
|
||||
|
||||
let betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
let betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => !!c.community?.community.instance_id,
|
||||
)
|
||||
).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
||||
// Create a root-level trunk-branch comment on alpha
|
||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
||||
// find that comment id on beta
|
||||
let betaComment = (
|
||||
await resolveComment(beta, commentRes.comment_view.comment)
|
||||
await waitUntil(
|
||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
||||
c => c.comment?.counts.score === 1,
|
||||
)
|
||||
).comment;
|
||||
|
||||
if (!betaComment) {
|
||||
|
@ -406,7 +423,10 @@ test("Reply to a comment from another instance, get notification", async () => {
|
|||
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// check inbox of replies on alpha, fetching read/unread both
|
||||
let alphaRepliesRes = await getReplies(alpha);
|
||||
let alphaRepliesRes = await waitUntil(
|
||||
() => getReplies(alpha),
|
||||
r => r.replies.length > 0,
|
||||
);
|
||||
const alphaReply = alphaRepliesRes.replies.find(
|
||||
r => r.comment.id === alphaComment.comment.id,
|
||||
);
|
||||
|
@ -423,6 +443,59 @@ test("Reply to a comment from another instance, get notification", async () => {
|
|||
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 () => {
|
||||
if (!betaCommunity) throw Error("no community");
|
||||
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
@ -740,3 +813,70 @@ test("Report a comment", async () => {
|
|||
);
|
||||
expect(betaReport.reason).toBe(alphaReport.reason);
|
||||
});
|
||||
|
||||
test("Dont send a comment reply to a blocked community", async () => {
|
||||
let newCommunity = await createCommunity(beta);
|
||||
let newCommunityId = newCommunity.community_view.community.id;
|
||||
|
||||
// Create a post on beta
|
||||
let betaPost = await createPost(beta, newCommunityId);
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, betaPost.post_view.post))!.post;
|
||||
if (!alphaPost) {
|
||||
throw "unable to locate post on alpha";
|
||||
}
|
||||
|
||||
// Check beta's inbox count
|
||||
let unreadCount = await getUnreadCount(beta);
|
||||
expect(unreadCount.replies).toBe(1);
|
||||
|
||||
// Beta blocks the new beta community
|
||||
let blockRes = await blockCommunity(beta, newCommunityId, true);
|
||||
expect(blockRes.blocked).toBe(true);
|
||||
delay();
|
||||
|
||||
// Alpha creates a comment
|
||||
let commentRes = await createComment(alpha, alphaPost.post.id);
|
||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
||||
let alphaComment = await resolveComment(
|
||||
beta,
|
||||
commentRes.comment_view.comment,
|
||||
);
|
||||
if (!alphaComment) {
|
||||
throw "Missing alpha comment before block";
|
||||
}
|
||||
|
||||
// Check beta's inbox count, make sure it stays the same
|
||||
unreadCount = await getUnreadCount(beta);
|
||||
expect(unreadCount.replies).toBe(1);
|
||||
|
||||
let replies = await getReplies(beta);
|
||||
expect(replies.replies.length).toBe(1);
|
||||
|
||||
// Unblock the community
|
||||
blockRes = await blockCommunity(beta, newCommunityId, false);
|
||||
expect(blockRes.blocked).toBe(false);
|
||||
});
|
||||
|
||||
/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also
|
||||
/// fetched recursively. Ensure that it works properly.
|
||||
test.skip("Fetch a deeply nested comment", async () => {
|
||||
let lastComment;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
let commentRes = await createComment(
|
||||
alpha,
|
||||
postOnAlphaRes.post_view.post.id,
|
||||
lastComment?.comment_view.comment.id,
|
||||
);
|
||||
expect(commentRes.comment_view.comment).toBeDefined();
|
||||
lastComment = commentRes;
|
||||
}
|
||||
|
||||
let betaComment = await resolveComment(
|
||||
beta,
|
||||
lastComment!.comment_view.comment,
|
||||
);
|
||||
|
||||
expect(betaComment!.comment!.comment).toBeDefined();
|
||||
expect(betaComment?.comment?.post).toBeDefined();
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import { AddModToCommunity } from "lemmy-js-client/dist/types/AddModToCommunity";
|
||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
||||
import {
|
||||
alpha,
|
||||
|
@ -9,6 +10,7 @@ import {
|
|||
resolveCommunity,
|
||||
createCommunity,
|
||||
deleteCommunity,
|
||||
delay,
|
||||
removeCommunity,
|
||||
getCommunity,
|
||||
followCommunity,
|
||||
|
@ -29,14 +31,14 @@ import {
|
|||
delta,
|
||||
betaAllowedInstances,
|
||||
searchPostLocal,
|
||||
resolveBetaCommunity,
|
||||
longDelay,
|
||||
editCommunity,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
import { EditSite, LemmyHttp } from "lemmy-js-client";
|
||||
import { EditCommunity, EditSite } from "lemmy-js-client";
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
});
|
||||
beforeAll(setupLogins);
|
||||
afterAll(unfollows);
|
||||
|
||||
function assertCommunityFederation(
|
||||
communityOne?: CommunityView,
|
||||
|
@ -66,8 +68,8 @@ test("Create community", async () => {
|
|||
|
||||
// A dupe check
|
||||
let prevName = communityRes.community_view.community.name;
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toBe(
|
||||
"community_already_exists",
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
||||
Error("community_already_exists"),
|
||||
);
|
||||
|
||||
// Cache the community on beta, make sure it has the other fields
|
||||
|
@ -242,7 +244,7 @@ test("Admin actions in remote community are not federated to origin", async () =
|
|||
);
|
||||
expect(banRes.banned).toBe(true);
|
||||
|
||||
// ban doesnt federate to community's origin instance alpha
|
||||
// ban doesn't federate to community's origin instance alpha
|
||||
let alphaPost = (await resolvePost(alpha, gammaPost.post)).post;
|
||||
expect(alphaPost?.creator_banned_from_community).toBe(false);
|
||||
|
||||
|
@ -253,10 +255,7 @@ test("Admin actions in remote community are not federated to origin", async () =
|
|||
|
||||
test("moderator view", async () => {
|
||||
// register a new user with their own community on alpha and post to it
|
||||
let registerUserRes = await registerUser(alpha);
|
||||
let otherUser = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: `Bearer ${registerUserRes.jwt ?? ""}` },
|
||||
});
|
||||
let otherUser = await registerUser(alpha, alphaUrl);
|
||||
|
||||
let otherCommunity = (await createCommunity(otherUser)).community_view;
|
||||
expect(otherCommunity.community.name).toBeDefined();
|
||||
|
@ -333,8 +332,8 @@ test("Get community for different casing on domain", async () => {
|
|||
|
||||
// A dupe check
|
||||
let prevName = communityRes.community_view.community.name;
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toBe(
|
||||
"community_already_exists",
|
||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
||||
Error("community_already_exists"),
|
||||
);
|
||||
|
||||
// Cache the community on beta, make sure it has the other fields
|
||||
|
@ -382,7 +381,9 @@ test("User blocks instance, communities are hidden", async () => {
|
|||
|
||||
test("Community follower count is federated", async () => {
|
||||
// Follow the beta community from alpha
|
||||
let resolved = await resolveBetaCommunity(alpha);
|
||||
let community = await createCommunity(beta);
|
||||
let communityActorId = community.community_view.community.actor_id;
|
||||
let resolved = await resolveCommunity(alpha, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -390,7 +391,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(alpha, true, resolved.community.community.id);
|
||||
let followed = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
() => resolveCommunity(alpha, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
@ -399,7 +400,7 @@ test("Community follower count is federated", async () => {
|
|||
expect(followed?.counts.subscribers).toBe(1);
|
||||
|
||||
// Follow the community from gamma
|
||||
resolved = await resolveBetaCommunity(gamma);
|
||||
resolved = await resolveCommunity(gamma, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -407,7 +408,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(gamma, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(gamma),
|
||||
() => resolveCommunity(gamma, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
@ -416,7 +417,7 @@ test("Community follower count is federated", async () => {
|
|||
expect(followed?.counts?.subscribers).toBe(2);
|
||||
|
||||
// Follow the community from delta
|
||||
resolved = await resolveBetaCommunity(delta);
|
||||
resolved = await resolveCommunity(delta, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -424,7 +425,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(delta, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(delta),
|
||||
() => resolveCommunity(delta, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
@ -453,7 +454,7 @@ test("Dont receive community activities after unsubscribe", async () => {
|
|||
);
|
||||
expect(communityRes1.community_view.counts.subscribers).toBe(2);
|
||||
|
||||
// temporarily block alpha, so that it doesnt know about unfollow
|
||||
// temporarily block alpha, so that it doesn't know about unfollow
|
||||
let editSiteForm: EditSite = {};
|
||||
editSiteForm.allowed_instances = ["lemmy-epsilon"];
|
||||
await beta.editSite(editSiteForm);
|
||||
|
@ -485,3 +486,90 @@ test("Dont receive community activities after unsubscribe", async () => {
|
|||
let postResBeta = searchPostLocal(beta, postRes.post_view.post);
|
||||
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("not_found"));
|
||||
|
||||
// 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("not_found"),
|
||||
);
|
||||
});
|
||||
|
||||
test("Remote mods can edit communities", async () => {
|
||||
let communityRes = await createCommunity(alpha);
|
||||
|
||||
let betaCommunity = await resolveCommunity(
|
||||
beta,
|
||||
communityRes.community_view.community.actor_id,
|
||||
);
|
||||
if (!betaCommunity.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let betaOnAlpha = await resolvePerson(alpha, "lemmy_beta@lemmy-beta:8551");
|
||||
|
||||
let form: AddModToCommunity = {
|
||||
community_id: communityRes.community_view.community.id,
|
||||
person_id: betaOnAlpha.person?.person.id as number,
|
||||
added: true,
|
||||
};
|
||||
alpha.addModToCommunity(form);
|
||||
|
||||
let form2: EditCommunity = {
|
||||
community_id: betaCommunity.community?.community.id as number,
|
||||
description: "Example description",
|
||||
};
|
||||
|
||||
await editCommunity(beta, form2);
|
||||
// give alpha time to get and process the edit
|
||||
await delay(1000);
|
||||
|
||||
let alphaCommunity = await getCommunity(
|
||||
alpha,
|
||||
communityRes.community_view.community.id,
|
||||
);
|
||||
|
||||
await expect(alphaCommunity.community_view.community.description).toBe(
|
||||
"Example description",
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,26 +5,65 @@ import {
|
|||
setupLogins,
|
||||
resolveBetaCommunity,
|
||||
followCommunity,
|
||||
unfollowRemotes,
|
||||
getSite,
|
||||
waitUntil,
|
||||
beta,
|
||||
betaUrl,
|
||||
registerUser,
|
||||
unfollows,
|
||||
delay,
|
||||
} from "./shared";
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
});
|
||||
beforeAll(setupLogins);
|
||||
|
||||
afterAll(async () => {
|
||||
await unfollowRemotes(alpha);
|
||||
afterAll(unfollows);
|
||||
|
||||
test("Follow local community", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
|
||||
let community = (await resolveBetaCommunity(user)).community!;
|
||||
let follow = await followCommunity(user, true, community.community.id);
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(follow.community_view.community.local).toBe(true);
|
||||
expect(follow.community_view.subscribed).toBe("Subscribed");
|
||||
expect(follow.community_view.counts.subscribers).toBe(
|
||||
community.counts.subscribers + 1,
|
||||
);
|
||||
expect(follow.community_view.counts.subscribers_local).toBe(
|
||||
community.counts.subscribers_local + 1,
|
||||
);
|
||||
|
||||
// Test an unfollow
|
||||
let unfollow = await followCommunity(user, false, community.community.id);
|
||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
||||
expect(unfollow.community_view.counts.subscribers).toBe(
|
||||
community.counts.subscribers,
|
||||
);
|
||||
expect(unfollow.community_view.counts.subscribers_local).toBe(
|
||||
community.counts.subscribers_local,
|
||||
);
|
||||
});
|
||||
|
||||
test("Follow federated community", async () => {
|
||||
let betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
if (!betaCommunity) {
|
||||
// It takes about 1 second for the community aggregates to federate
|
||||
await delay(2000); // if this is the second test run, we don't have a way to wait for the correct number of subscribers
|
||||
const betaCommunityInitial = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => !!c.community && c.community?.counts.subscribers >= 1,
|
||||
)
|
||||
).community;
|
||||
if (!betaCommunityInitial) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followCommunity(alpha, true, betaCommunity.community.id);
|
||||
betaCommunity = (
|
||||
let follow = await followCommunity(
|
||||
alpha,
|
||||
true,
|
||||
betaCommunityInitial.community.id,
|
||||
);
|
||||
expect(follow.community_view.subscribed).toBe("Pending");
|
||||
const betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
|
@ -35,14 +74,24 @@ test("Follow federated community", async () => {
|
|||
expect(betaCommunity?.community.local).toBe(false);
|
||||
expect(betaCommunity?.community.name).toBe("main");
|
||||
expect(betaCommunity?.subscribed).toBe("Subscribed");
|
||||
expect(betaCommunity?.counts.subscribers_local).toBe(
|
||||
betaCommunityInitial.counts.subscribers_local + 1,
|
||||
);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta1 = await resolveBetaCommunity(beta);
|
||||
expect(communityOnBeta1.community?.counts.subscribers).toBe(
|
||||
betaCommunityInitial.counts.subscribers + 1,
|
||||
);
|
||||
|
||||
// Check it from local
|
||||
let site = await getSite(alpha);
|
||||
let remoteCommunityId = site.my_user?.follows.find(
|
||||
c => c.community.local == false,
|
||||
c =>
|
||||
c.community.local == false &&
|
||||
c.community.id === betaCommunityInitial.community.id,
|
||||
)?.community.id;
|
||||
expect(remoteCommunityId).toBeDefined();
|
||||
expect(site.my_user?.follows.length).toBe(2);
|
||||
|
||||
if (!remoteCommunityId) {
|
||||
throw "Missing remote community id";
|
||||
|
@ -54,5 +103,21 @@ test("Follow federated community", async () => {
|
|||
|
||||
// Make sure you are unsubbed locally
|
||||
let siteUnfollowCheck = await getSite(alpha);
|
||||
expect(siteUnfollowCheck.my_user?.follows.length).toBe(1);
|
||||
expect(
|
||||
siteUnfollowCheck.my_user?.follows.find(
|
||||
c => c.community.id === betaCommunityInitial.community.id,
|
||||
),
|
||||
).toBe(undefined);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta2 = await waitUntil(
|
||||
() => resolveBetaCommunity(beta),
|
||||
c =>
|
||||
c.community?.counts.subscribers ===
|
||||
betaCommunityInitial.counts.subscribers,
|
||||
);
|
||||
expect(communityOnBeta2.community?.counts.subscribers).toBe(
|
||||
betaCommunityInitial.counts.subscribers,
|
||||
);
|
||||
expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1);
|
||||
});
|
||||
|
|
372
api_tests/src/image.spec.ts
Normal file
372
api_tests/src/image.spec.ts
Normal file
|
@ -0,0 +1,372 @@
|
|||
jest.setTimeout(120000);
|
||||
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
PurgePerson,
|
||||
PurgePost,
|
||||
} from "lemmy-js-client";
|
||||
import {
|
||||
alpha,
|
||||
alphaImage,
|
||||
alphaUrl,
|
||||
beta,
|
||||
betaUrl,
|
||||
createCommunity,
|
||||
createPost,
|
||||
deleteAllImages,
|
||||
epsilon,
|
||||
followCommunity,
|
||||
gamma,
|
||||
getSite,
|
||||
imageFetchLimit,
|
||||
registerUser,
|
||||
resolveBetaCommunity,
|
||||
resolveCommunity,
|
||||
resolvePost,
|
||||
setupLogins,
|
||||
waitForPost,
|
||||
unfollows,
|
||||
getPost,
|
||||
waitUntil,
|
||||
createPostWithThumbnail,
|
||||
sampleImage,
|
||||
sampleSite,
|
||||
} from "./shared";
|
||||
|
||||
beforeAll(setupLogins);
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([unfollows(), deleteAllImages(alpha)]);
|
||||
});
|
||||
|
||||
test("Upload image and delete it", async () => {
|
||||
// Before running this test, you need to delete all previous images in the DB
|
||||
await deleteAllImages(alpha);
|
||||
|
||||
// Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
|
||||
// in testing mode.
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await alphaImage.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const response = await fetch(upload.url ?? "");
|
||||
const content = await response.text();
|
||||
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
|
||||
const delete_form: DeleteImage = {
|
||||
token: upload.files![0].delete_token,
|
||||
filename: upload.files![0].file,
|
||||
};
|
||||
const delete_ = await alphaImage.deleteImage(delete_form);
|
||||
expect(delete_).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const response2 = await fetch(upload.url ?? "");
|
||||
const content2 = await response2.text();
|
||||
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 () => {
|
||||
let user = await registerUser(alphaImage, alphaUrl);
|
||||
|
||||
// upload test image
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const response = await fetch(upload.url ?? "");
|
||||
const content = await response.text();
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
// purge user
|
||||
let site = await getSite(user);
|
||||
const purgeForm: PurgePerson = {
|
||||
person_id: site.my_user!.local_user_view.person.id,
|
||||
};
|
||||
const delete_ = await alphaImage.purgePerson(purgeForm);
|
||||
expect(delete_.success).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const response2 = await fetch(upload.url ?? "");
|
||||
const content2 = await response2.text();
|
||||
expect(content2).toBe("");
|
||||
});
|
||||
|
||||
test("Purge post, linked image removed", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
|
||||
// upload test image
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
expect(upload.files![0].file).toBeDefined();
|
||||
expect(upload.files![0].delete_token).toBeDefined();
|
||||
expect(upload.url).toBeDefined();
|
||||
expect(upload.delete_url).toBeDefined();
|
||||
|
||||
// ensure that image download is working. theres probably a better way to do this
|
||||
const response = await fetch(upload.url ?? "");
|
||||
const content = await response.text();
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
|
||||
let community = await resolveBetaCommunity(user);
|
||||
let post = await createPost(
|
||||
user,
|
||||
community.community!.community.id,
|
||||
upload.url,
|
||||
);
|
||||
expect(post.post_view.post.url).toBe(upload.url);
|
||||
expect(post.post_view.image_details).toBeDefined();
|
||||
|
||||
// purge post
|
||||
const purgeForm: PurgePost = {
|
||||
post_id: post.post_view.post.id,
|
||||
};
|
||||
const delete_ = await beta.purgePost(purgeForm);
|
||||
expect(delete_.success).toBe(true);
|
||||
|
||||
// ensure that image is deleted
|
||||
const response2 = await fetch(upload.url ?? "");
|
||||
const content2 = await response2.text();
|
||||
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);
|
||||
});
|
|
@ -18,12 +18,12 @@ import {
|
|||
resolveBetaCommunity,
|
||||
createComment,
|
||||
deletePost,
|
||||
delay,
|
||||
removePost,
|
||||
getPost,
|
||||
unfollowRemotes,
|
||||
resolvePerson,
|
||||
banPersonFromSite,
|
||||
searchPostLocal,
|
||||
followCommunity,
|
||||
banPersonFromCommunity,
|
||||
reportPost,
|
||||
|
@ -37,9 +37,10 @@ import {
|
|||
waitForPost,
|
||||
alphaUrl,
|
||||
loginUser,
|
||||
createCommunity,
|
||||
} from "./shared";
|
||||
import { PostView } from "lemmy-js-client/dist/types/PostView";
|
||||
import { LemmyHttp } from "lemmy-js-client";
|
||||
import { EditSite, ResolveObject } from "lemmy-js-client";
|
||||
|
||||
let betaCommunity: CommunityView | undefined;
|
||||
|
||||
|
@ -47,14 +48,28 @@ beforeAll(async () => {
|
|||
await setupLogins();
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
expect(betaCommunity).toBeDefined();
|
||||
await unfollows();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await unfollows();
|
||||
});
|
||||
afterAll(unfollows);
|
||||
|
||||
async function assertPostFederation(
|
||||
postOne: PostView,
|
||||
postTwo: PostView,
|
||||
waitForMeta = true,
|
||||
) {
|
||||
// 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.
|
||||
if (waitForMeta) {
|
||||
postOne = await waitForPost(beta, postOne.post, res => {
|
||||
return res === null || !!res?.post.embed_title;
|
||||
});
|
||||
postTwo = await waitForPost(
|
||||
beta,
|
||||
postTwo.post,
|
||||
res => res === null || !!res?.post.embed_title,
|
||||
);
|
||||
}
|
||||
|
||||
function assertPostFederation(postOne?: PostView, postTwo?: PostView) {
|
||||
expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id);
|
||||
expect(postOne?.post.name).toBe(postTwo?.post.name);
|
||||
expect(postOne?.post.body).toBe(postTwo?.post.body);
|
||||
|
@ -72,11 +87,23 @@ function assertPostFederation(postOne?: PostView, postTwo?: PostView) {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
let postRes = await createPost(
|
||||
alpha,
|
||||
betaCommunity.community.id,
|
||||
"https://example.com/",
|
||||
"აშშ ითხოვს ირანს დაუყოვნებლივ გაანთავისუფლოს დაკავებული ნავთობის ტანკერი",
|
||||
);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
expect(postRes.post_view.community.local).toBe(false);
|
||||
expect(postRes.post_view.creator.local).toBe(true);
|
||||
|
@ -93,21 +120,27 @@ test("Create a post", async () => {
|
|||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.counts.score).toBe(1);
|
||||
assertPostFederation(betaPost, postRes.post_view);
|
||||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
|
||||
// Delta only follows beta, so it should not see an alpha ap_id
|
||||
await expect(resolvePost(delta, postRes.post_view.post)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
);
|
||||
await expect(
|
||||
resolvePost(delta, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(Error("not_found"));
|
||||
|
||||
// Epsilon has alpha blocked, it should not see the alpha post
|
||||
await expect(resolvePost(epsilon, postRes.post_view.post)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
);
|
||||
await expect(
|
||||
resolvePost(epsilon, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(Error("not_found"));
|
||||
|
||||
// remove added allow/blocklists
|
||||
editSiteForm.allowed_instances = [];
|
||||
editSiteForm.blocked_instances = [];
|
||||
await delta.editSite(editSiteForm);
|
||||
await epsilon.editSite(editSiteForm);
|
||||
});
|
||||
|
||||
test("Create a post in a non-existent community", async () => {
|
||||
await expect(createPost(alpha, -2)).rejects.toBe("couldnt_find_community");
|
||||
await expect(createPost(alpha, -2)).rejects.toStrictEqual(Error("not_found"));
|
||||
});
|
||||
|
||||
test("Unlike a post", async () => {
|
||||
|
@ -133,7 +166,7 @@ test("Unlike a post", async () => {
|
|||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.counts.score).toBe(0);
|
||||
assertPostFederation(betaPost, postRes.post_view);
|
||||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
});
|
||||
|
||||
test("Update a post", async () => {
|
||||
|
@ -154,11 +187,11 @@ test("Update a post", async () => {
|
|||
expect(betaPost.community.local).toBe(true);
|
||||
expect(betaPost.creator.local).toBe(false);
|
||||
expect(betaPost.post.name).toBe(updatedName);
|
||||
assertPostFederation(betaPost, updatedPost.post_view);
|
||||
await assertPostFederation(betaPost, updatedPost.post_view);
|
||||
|
||||
// Make sure lemmy beta cannot update the post
|
||||
await expect(editPost(beta, betaPost.post)).rejects.toBe(
|
||||
"no_post_edit_allowed",
|
||||
await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual(
|
||||
Error("no_post_edit_allowed"),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -196,12 +229,35 @@ test("Sticky a post", async () => {
|
|||
if (!gammaPost) {
|
||||
throw "Missing gamma post";
|
||||
}
|
||||
let gammaTrySticky = await featurePost(gamma, true, gammaPost.post);
|
||||
// This has been failing occasionally
|
||||
await featurePost(gamma, true, gammaPost.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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
|
@ -225,8 +281,12 @@ test("Lock a post", async () => {
|
|||
post => !!post && post.post.locked,
|
||||
);
|
||||
|
||||
// Try to make a new comment there, on alpha
|
||||
await expect(createComment(alpha, alphaPost1.post.id)).rejects.toBe("locked");
|
||||
// Try to make a new comment there, on alpha. For this we need to create a normal
|
||||
// user account because admins/mods can comment in locked posts.
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
await expect(createComment(user, alphaPost1.post.id)).rejects.toStrictEqual(
|
||||
Error("locked"),
|
||||
);
|
||||
|
||||
// Unlock a post
|
||||
let unlockedPost = await lockPost(beta, false, betaPost1.post);
|
||||
|
@ -243,7 +303,7 @@ test("Lock a post", async () => {
|
|||
expect(alphaPost2.post.locked).toBe(false);
|
||||
|
||||
// Try to create a new comment, on alpha
|
||||
let commentAlpha = await createComment(alpha, alphaPost1.post.id);
|
||||
let commentAlpha = await createComment(user, alphaPost1.post.id);
|
||||
expect(commentAlpha).toBeDefined();
|
||||
});
|
||||
|
||||
|
@ -278,11 +338,11 @@ test("Delete a post", async () => {
|
|||
throw "Missing beta post 2";
|
||||
}
|
||||
expect(betaPost2.post.deleted).toBe(false);
|
||||
assertPostFederation(betaPost2, undeletedPost.post_view);
|
||||
await assertPostFederation(betaPost2, undeletedPost.post_view);
|
||||
|
||||
// Make sure lemmy beta cannot delete the post
|
||||
await expect(deletePost(beta, true, betaPost2.post)).rejects.toBe(
|
||||
"no_post_edit_allowed",
|
||||
await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual(
|
||||
Error("no_post_edit_allowed"),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -321,7 +381,7 @@ test("Remove a post from admin and community on different instance", async () =>
|
|||
// Make sure lemmy beta sees post is undeleted
|
||||
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
|
||||
expect(betaPost2?.post.removed).toBe(false);
|
||||
assertPostFederation(betaPost2, undeletedPost.post_view);
|
||||
await assertPostFederation(betaPost2!, undeletedPost.post_view);
|
||||
});
|
||||
|
||||
test("Remove a post from admin and community on same instance", async () => {
|
||||
|
@ -352,7 +412,11 @@ test("Remove a post from admin and community on same instance", async () => {
|
|||
p => p?.post_view.post.removed ?? false,
|
||||
);
|
||||
expect(alphaPost?.post_view.post.removed).toBe(true);
|
||||
assertPostFederation(alphaPost.post_view, removePostRes.post_view);
|
||||
await assertPostFederation(
|
||||
alphaPost.post_view,
|
||||
removePostRes.post_view,
|
||||
false,
|
||||
);
|
||||
|
||||
// Undelete
|
||||
let undeletedPost = await removePost(beta, false, betaPost.post);
|
||||
|
@ -365,7 +429,7 @@ test("Remove a post from admin and community on same instance", async () => {
|
|||
p => !!p && !p.post.removed,
|
||||
);
|
||||
expect(alphaPost2.post.removed).toBe(false);
|
||||
assertPostFederation(alphaPost2, undeletedPost.post_view);
|
||||
await assertPostFederation(alphaPost2, undeletedPost.post_view);
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
|
@ -381,34 +445,34 @@ test("Search for a post", async () => {
|
|||
expect(betaPost?.post.name).toBeDefined();
|
||||
});
|
||||
|
||||
test("Enforce site ban for federated user", async () => {
|
||||
test("Enforce site ban federation for local user", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
||||
// create a test user
|
||||
let alphaUserJwt = await registerUser(alpha);
|
||||
expect(alphaUserJwt).toBeDefined();
|
||||
let alpha_user = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: `Bearer ${alphaUserJwt.jwt ?? ""}` },
|
||||
});
|
||||
let alphaUserPerson = (await getSite(alpha_user)).my_user?.local_user_view
|
||||
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();
|
||||
let alphaPerson = (await resolvePerson(alpha_user, alphaUserActorId!)).person;
|
||||
await followBeta(alphaUserHttp);
|
||||
|
||||
let alphaPerson = (await resolvePerson(alphaUserHttp, alphaUserActorId!))
|
||||
.person;
|
||||
if (!alphaPerson) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
expect(alphaPerson).toBeDefined();
|
||||
|
||||
// alpha makes post in beta community, it federates to beta instance
|
||||
let postRes1 = await createPost(alpha_user, betaCommunity.community.id);
|
||||
let postRes1 = await createPost(alphaUserHttp, betaCommunity.community.id);
|
||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
||||
|
||||
// ban alpha from its instance
|
||||
// ban alpha from its own instance
|
||||
let banAlpha = await banPersonFromSite(
|
||||
alpha,
|
||||
alphaPerson.person.id,
|
||||
|
@ -425,40 +489,111 @@ test("Enforce site ban for federated user", async () => {
|
|||
expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
|
||||
|
||||
// existing alpha post should be removed on beta
|
||||
await waitUntil(
|
||||
let betaBanRes = await waitUntil(
|
||||
() => getPost(beta, searchBeta1.post.id),
|
||||
s => s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(true);
|
||||
|
||||
// Unban alpha
|
||||
let unBanAlpha = await banPersonFromSite(
|
||||
alpha,
|
||||
alphaPerson.person.id,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
expect(unBanAlpha.banned).toBe(false);
|
||||
|
||||
// existing alpha post should be restored on beta
|
||||
betaBanRes = await waitUntil(
|
||||
() => getPost(beta, searchBeta1.post.id),
|
||||
s => !s.post_view.post.removed,
|
||||
);
|
||||
expect(betaBanRes.post_view.post.removed).toBe(false);
|
||||
|
||||
// Login gets invalidated by ban, need to login again
|
||||
if (!alphaUserPerson) {
|
||||
throw "Missing alpha person";
|
||||
}
|
||||
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
|
||||
alpha_user.setHeaders({
|
||||
Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "",
|
||||
alphaUserHttp.setHeaders({
|
||||
Authorization: "Bearer " + newAlphaUserJwt.jwt,
|
||||
});
|
||||
// alpha makes new post in beta community, it federates
|
||||
let postRes2 = await createPost(alpha_user, betaCommunity!.community.id);
|
||||
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id);
|
||||
await waitForPost(beta, postRes2.post_view.post);
|
||||
|
||||
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId!);
|
||||
expect(alphaUserOnBeta2.person?.person.banned).toBe(false);
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test.skip("Enforce community ban for federated user", async () => {
|
||||
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!);
|
||||
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 () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followBeta(alpha);
|
||||
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
|
||||
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person;
|
||||
if (!alphaPerson) {
|
||||
|
@ -468,38 +603,46 @@ test.skip("Enforce community ban for federated user", async () => {
|
|||
|
||||
// make a post in beta, it goes through
|
||||
let postRes1 = await createPost(alpha, betaCommunity.community.id);
|
||||
let searchBeta1 = await searchPostLocal(beta, postRes1.post_view.post);
|
||||
expect(searchBeta1.posts[0]).toBeDefined();
|
||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
||||
expect(searchBeta1.post).toBeDefined();
|
||||
|
||||
// ban alpha from beta community
|
||||
let banAlpha = await banPersonFromCommunity(
|
||||
beta,
|
||||
alphaPerson.person.id,
|
||||
2,
|
||||
searchBeta1.community.id,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(banAlpha.banned).toBe(true);
|
||||
|
||||
// ensure that the post by alpha got removed
|
||||
await expect(getPost(alpha, searchBeta1.posts[0].post.id)).rejects.toBe(
|
||||
"unknown",
|
||||
let removePostRes = await waitUntil(
|
||||
() => getPost(alpha, postRes1.post_view.post.id),
|
||||
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
|
||||
await expect(createPost(alpha, betaCommunity.community.id)).rejects.toBe(
|
||||
"banned_from_community",
|
||||
);
|
||||
await expect(
|
||||
createPost(alpha, betaCommunity.community.id),
|
||||
).rejects.toStrictEqual(Error("person_is_banned_from_community"));
|
||||
|
||||
// Unban alpha
|
||||
let unBanAlpha = await banPersonFromCommunity(
|
||||
beta,
|
||||
alphaPerson.person.id,
|
||||
2,
|
||||
searchBeta1.community.id,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
expect(unBanAlpha.banned).toBe(false);
|
||||
|
||||
// Need to re-follow the community
|
||||
await followBeta(alpha);
|
||||
|
||||
let postRes3 = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes3.post_view.post).toBeDefined();
|
||||
expect(postRes3.post_view.community.local).toBe(false);
|
||||
|
@ -507,52 +650,173 @@ test.skip("Enforce community ban for federated user", async () => {
|
|||
expect(postRes3.post_view.counts.score).toBe(1);
|
||||
|
||||
// Make sure that post makes it to beta community
|
||||
let searchBeta2 = await searchPostLocal(beta, postRes3.post_view.post);
|
||||
expect(searchBeta2.posts[0]).toBeDefined();
|
||||
let postRes4 = await waitForPost(beta, postRes3.post_view.post);
|
||||
expect(postRes4.post).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 () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
await followBeta(alpha);
|
||||
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post;
|
||||
expect(betaPost?.post.name).toBeDefined();
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
|
||||
test("Report a post", async () => {
|
||||
// Note, this is a different one from the setup
|
||||
let betaCommunity = (await resolveBetaCommunity(beta)).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postRes = await createPost(beta, betaCommunity.community.id);
|
||||
// Create post from alpha
|
||||
let alphaCommunity = (await resolveBetaCommunity(alpha)).community!;
|
||||
await followBeta(alpha);
|
||||
let postRes = await createPost(alpha, alphaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
|
||||
if (!alphaPost) {
|
||||
throw "Missing alpha post";
|
||||
}
|
||||
let alphaReport = (
|
||||
await reportPost(alpha, alphaPost.post.id, randomString(10))
|
||||
).post_report_view.post_report;
|
||||
|
||||
// Send report from gamma
|
||||
let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!;
|
||||
let gammaReport = (
|
||||
await reportPost(gamma, gammaPost.post.id, randomString(10))
|
||||
).post_report_view.post_report;
|
||||
expect(gammaReport).toBeDefined();
|
||||
|
||||
// Report was federated to community instance
|
||||
let betaReport = (await waitUntil(
|
||||
() =>
|
||||
listPostReports(beta).then(p =>
|
||||
p.post_reports.find(
|
||||
r =>
|
||||
r.post_report.original_post_name === alphaReport.original_post_name,
|
||||
r.post_report.original_post_name === gammaReport.original_post_name,
|
||||
),
|
||||
),
|
||||
res => !!res,
|
||||
))!.post_report;
|
||||
expect(betaReport).toBeDefined();
|
||||
expect(betaReport.resolved).toBe(false);
|
||||
expect(betaReport.original_post_name).toBe(alphaReport.original_post_name);
|
||||
expect(betaReport.original_post_url).toBe(alphaReport.original_post_url);
|
||||
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
|
||||
expect(betaReport.reason).toBe(alphaReport.reason);
|
||||
expect(betaReport.original_post_name).toBe(gammaReport.original_post_name);
|
||||
//expect(betaReport.original_post_url).toBe(gammaReport.original_post_url);
|
||||
expect(betaReport.original_post_body).toBe(gammaReport.original_post_body);
|
||||
expect(betaReport.reason).toBe(gammaReport.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 () => {
|
||||
await followBeta(alpha);
|
||||
let alphaPost = await createPost(alpha, betaCommunity!.community.id);
|
||||
expect(alphaPost.post_view.post).toBeDefined();
|
||||
// Make sure that post is liked on beta
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
alphaPost.post_view.post,
|
||||
res => res?.counts.score === 1,
|
||||
);
|
||||
|
||||
expect(betaPost).toBeDefined();
|
||||
expect(betaPost.post?.ap_id).toBe(alphaPost.post_view.post.ap_id);
|
||||
|
||||
// Fetch post from url on beta instance instead of ap_id
|
||||
let q = `http://lemmy-beta:8551/post/${betaPost.post.id}`;
|
||||
let form: ResolveObject = {
|
||||
q,
|
||||
};
|
||||
let gammaPost = await gamma.resolveObject(form);
|
||||
expect(gammaPost).toBeDefined();
|
||||
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();
|
||||
});
|
||||
|
||||
test("Rewrite markdown links", async () => {
|
||||
const community = (await resolveBetaCommunity(beta)).community!;
|
||||
|
||||
// create a post
|
||||
let postRes1 = await createPost(beta, community.community.id);
|
||||
|
||||
// link to this post in markdown
|
||||
let postRes2 = await createPost(
|
||||
beta,
|
||||
community.community.id,
|
||||
"https://example.com/",
|
||||
`[link](${postRes1.post_view.post.ap_id})`,
|
||||
);
|
||||
console.log(postRes2.post_view.post.body);
|
||||
expect(postRes2.post_view.post).toBeDefined();
|
||||
|
||||
// fetch both posts from another instance
|
||||
const alphaPost1 = await resolvePost(alpha, postRes1.post_view.post);
|
||||
const alphaPost2 = await resolvePost(alpha, postRes2.post_view.post);
|
||||
|
||||
// remote markdown link is replaced with local link
|
||||
expect(alphaPost2.post?.post.body).toBe(
|
||||
`[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
editPrivateMessage,
|
||||
listPrivateMessages,
|
||||
deletePrivateMessage,
|
||||
unfollowRemotes,
|
||||
waitUntil,
|
||||
reportPrivateMessage,
|
||||
unfollows,
|
||||
} from "./shared";
|
||||
|
||||
let recipient_id: number;
|
||||
|
@ -20,9 +21,7 @@ beforeAll(async () => {
|
|||
recipient_id = 3;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await unfollowRemotes(alpha);
|
||||
});
|
||||
afterAll(unfollows);
|
||||
|
||||
test("Create a private message", async () => {
|
||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||
|
@ -109,3 +108,42 @@ test("Delete a private message", async () => {
|
|||
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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import {
|
||||
BlockCommunity,
|
||||
BlockCommunityResponse,
|
||||
BlockInstance,
|
||||
BlockInstanceResponse,
|
||||
CommunityId,
|
||||
CreatePrivateMessageReport,
|
||||
DeleteImage,
|
||||
EditCommunity,
|
||||
GetReplies,
|
||||
GetRepliesResponse,
|
||||
GetUnreadCountResponse,
|
||||
InstanceId,
|
||||
LemmyHttp,
|
||||
PostView,
|
||||
PrivateMessageReportResponse,
|
||||
SuccessResponse,
|
||||
} from "lemmy-js-client";
|
||||
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
|
||||
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
|
||||
|
@ -55,7 +63,6 @@ import { Register } from "lemmy-js-client/dist/types/Register";
|
|||
import { SaveUserSettings } from "lemmy-js-client/dist/types/SaveUserSettings";
|
||||
import { DeleteAccount } from "lemmy-js-client/dist/types/DeleteAccount";
|
||||
import { GetSiteResponse } from "lemmy-js-client/dist/types/GetSiteResponse";
|
||||
import { DeleteAccountResponse } from "lemmy-js-client/dist/types/DeleteAccountResponse";
|
||||
import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessagesResponse";
|
||||
import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages";
|
||||
import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse";
|
||||
|
@ -72,19 +79,26 @@ import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDe
|
|||
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
|
||||
import { ListingType } from "lemmy-js-client/dist/types/ListingType";
|
||||
|
||||
export let alphaUrl = "http://127.0.0.1:8541";
|
||||
export let betaUrl = "http://127.0.0.1:8551";
|
||||
export let gammaUrl = "http://127.0.0.1:8561";
|
||||
export let deltaUrl = "http://127.0.0.1:8571";
|
||||
export let epsilonUrl = "http://127.0.0.1:8581";
|
||||
export const fetchFunction = fetch;
|
||||
export const imageFetchLimit = 50;
|
||||
export const sampleImage =
|
||||
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg";
|
||||
export const sampleSite = "https://yahoo.com";
|
||||
|
||||
export let alpha = new LemmyHttp(alphaUrl);
|
||||
export let beta = new LemmyHttp(betaUrl);
|
||||
export let gamma = new LemmyHttp(gammaUrl);
|
||||
export let delta = new LemmyHttp(deltaUrl);
|
||||
export let epsilon = new LemmyHttp(epsilonUrl);
|
||||
export const alphaUrl = "http://127.0.0.1:8541";
|
||||
export const betaUrl = "http://127.0.0.1:8551";
|
||||
export const gammaUrl = "http://127.0.0.1:8561";
|
||||
export const deltaUrl = "http://127.0.0.1:8571";
|
||||
export const epsilonUrl = "http://127.0.0.1:8581";
|
||||
|
||||
export let betaAllowedInstances = [
|
||||
export const alpha = new LemmyHttp(alphaUrl, { fetchFunction });
|
||||
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-gamma",
|
||||
"lemmy-delta",
|
||||
|
@ -132,6 +146,7 @@ export async function setupLogins() {
|
|||
resEpsilon,
|
||||
]);
|
||||
alpha.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
|
||||
alphaImage.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
|
||||
beta.setHeaders({ Authorization: `Bearer ${res[1].jwt ?? ""}` });
|
||||
gamma.setHeaders({ Authorization: `Bearer ${res[2].jwt ?? ""}` });
|
||||
delta.setHeaders({ Authorization: `Bearer ${res[3].jwt ?? ""}` });
|
||||
|
@ -168,13 +183,10 @@ export async function setupLogins() {
|
|||
];
|
||||
await gamma.editSite(editSiteForm);
|
||||
|
||||
// Setup delta allowed instance
|
||||
editSiteForm.allowed_instances = ["lemmy-beta"];
|
||||
await delta.editSite(editSiteForm);
|
||||
|
||||
editSiteForm.allowed_instances = [];
|
||||
editSiteForm.blocked_instances = ["lemmy-alpha"];
|
||||
await epsilon.editSite(editSiteForm);
|
||||
|
||||
// Create the main alpha/beta communities
|
||||
// Ignore thrown errors of duplicates
|
||||
try {
|
||||
|
@ -185,7 +197,7 @@ export async function setupLogins() {
|
|||
// (because last_successful_id is set to current id when federation to an instance is first started)
|
||||
// only needed the first time so do in this try
|
||||
await delay(10_000);
|
||||
} catch (_) {
|
||||
} catch {
|
||||
console.log("Communities already exist");
|
||||
}
|
||||
}
|
||||
|
@ -193,17 +205,20 @@ export async function setupLogins() {
|
|||
export async function createPost(
|
||||
api: LemmyHttp,
|
||||
community_id: number,
|
||||
url: string = "https://example.com/",
|
||||
body = randomString(10),
|
||||
// use example.com for consistent title and embed description
|
||||
name: string = randomString(5),
|
||||
alt_text = randomString(10),
|
||||
custom_thumbnail: string | undefined = undefined,
|
||||
): Promise<PostResponse> {
|
||||
let name = randomString(5);
|
||||
let body = randomString(10);
|
||||
// switch from google.com to example.com for consistent title (embed_title and embed_description)
|
||||
// google switches description when a google doodle appears
|
||||
let url = "https://example.com/";
|
||||
let form: CreatePost = {
|
||||
name,
|
||||
url,
|
||||
body,
|
||||
alt_text,
|
||||
community_id,
|
||||
custom_thumbnail,
|
||||
};
|
||||
return api.createPost(form);
|
||||
}
|
||||
|
@ -220,6 +235,21 @@ export async function editPost(
|
|||
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(
|
||||
api: LemmyHttp,
|
||||
deleted: boolean,
|
||||
|
@ -287,6 +317,7 @@ export async function searchPostLocal(
|
|||
q: post.name,
|
||||
type_: "Posts",
|
||||
sort: "TopAll",
|
||||
listing_type: "All",
|
||||
};
|
||||
return api.search(form);
|
||||
}
|
||||
|
@ -322,6 +353,7 @@ export async function getComments(
|
|||
post_id: post_id,
|
||||
type_: listingType,
|
||||
sort: "New",
|
||||
limit: 50,
|
||||
};
|
||||
return api.getComments(form);
|
||||
}
|
||||
|
@ -332,10 +364,13 @@ export async function getUnreadCount(
|
|||
return api.getUnreadCount();
|
||||
}
|
||||
|
||||
export async function getReplies(api: LemmyHttp): Promise<GetRepliesResponse> {
|
||||
export async function getReplies(
|
||||
api: LemmyHttp,
|
||||
unread_only: boolean = false,
|
||||
): Promise<GetRepliesResponse> {
|
||||
let form: GetReplies = {
|
||||
sort: "New",
|
||||
unread_only: false,
|
||||
unread_only,
|
||||
};
|
||||
return api.getReplies(form);
|
||||
}
|
||||
|
@ -384,13 +419,13 @@ export async function banPersonFromSite(
|
|||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
ban: boolean,
|
||||
remove_data: boolean,
|
||||
remove_or_restore_data: boolean,
|
||||
): Promise<BanPersonResponse> {
|
||||
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
|
||||
let form: BanPerson = {
|
||||
person_id,
|
||||
ban,
|
||||
remove_data: remove_data,
|
||||
remove_or_restore_data,
|
||||
};
|
||||
return api.banPerson(form);
|
||||
}
|
||||
|
@ -399,13 +434,13 @@ export async function banPersonFromCommunity(
|
|||
api: LemmyHttp,
|
||||
person_id: number,
|
||||
community_id: number,
|
||||
remove_data: boolean,
|
||||
remove_or_restore_data: boolean,
|
||||
ban: boolean,
|
||||
): Promise<BanFromCommunityResponse> {
|
||||
let form: BanFromCommunity = {
|
||||
person_id,
|
||||
community_id,
|
||||
remove_data: remove_data,
|
||||
remove_or_restore_data,
|
||||
ban,
|
||||
};
|
||||
return api.banFromCommunity(form);
|
||||
|
@ -422,8 +457,9 @@ export async function followCommunity(
|
|||
};
|
||||
const res = await api.followCommunity(form);
|
||||
await waitUntil(
|
||||
() => resolveCommunity(api, res.community_view.community.actor_id),
|
||||
g => g.community?.subscribed === (follow ? "Subscribed" : "NotSubscribed"),
|
||||
() => getCommunity(api, res.community_view.community.id),
|
||||
g =>
|
||||
g.community_view.subscribed === (follow ? "Subscribed" : "NotSubscribed"),
|
||||
);
|
||||
// wait FOLLOW_ADDITIONS_RECHECK_DELAY (there's no API to wait for this currently)
|
||||
await delay(2000);
|
||||
|
@ -517,7 +553,7 @@ export async function likeComment(
|
|||
|
||||
export async function createCommunity(
|
||||
api: LemmyHttp,
|
||||
name_: string = randomString(5),
|
||||
name_: string = randomString(10),
|
||||
): Promise<CommunityResponse> {
|
||||
let description = "a sample description";
|
||||
let form: CreateCommunity = {
|
||||
|
@ -528,6 +564,13 @@ export async function createCommunity(
|
|||
return api.createCommunity(form);
|
||||
}
|
||||
|
||||
export async function editCommunity(
|
||||
api: LemmyHttp,
|
||||
form: EditCommunity,
|
||||
): Promise<CommunityResponse> {
|
||||
return api.editCommunity(form);
|
||||
}
|
||||
|
||||
export async function getCommunity(
|
||||
api: LemmyHttp,
|
||||
id: number,
|
||||
|
@ -610,15 +653,22 @@ export async function deletePrivateMessage(
|
|||
|
||||
export async function registerUser(
|
||||
api: LemmyHttp,
|
||||
url: string,
|
||||
username: string = randomString(5),
|
||||
): Promise<LoginResponse> {
|
||||
): Promise<LemmyHttp> {
|
||||
let form: Register = {
|
||||
username,
|
||||
password,
|
||||
password_verify: password,
|
||||
show_nsfw: true,
|
||||
};
|
||||
return api.register(form);
|
||||
let login_response = await api.register(form);
|
||||
|
||||
expect(login_response.jwt).toBeDefined();
|
||||
let lemmy_http = new LemmyHttp(url, {
|
||||
headers: { Authorization: `Bearer ${login_response.jwt ?? ""}` },
|
||||
});
|
||||
return lemmy_http;
|
||||
}
|
||||
|
||||
export async function loginUser(
|
||||
|
@ -634,13 +684,13 @@ export async function loginUser(
|
|||
|
||||
export async function saveUserSettingsBio(
|
||||
api: LemmyHttp,
|
||||
): Promise<LoginResponse> {
|
||||
): Promise<SuccessResponse> {
|
||||
let form: SaveUserSettings = {
|
||||
show_nsfw: true,
|
||||
blur_nsfw: false,
|
||||
auto_expand: true,
|
||||
theme: "darkly",
|
||||
default_sort_type: "Active",
|
||||
default_post_sort_type: "Active",
|
||||
default_listing_type: "All",
|
||||
interface_language: "en",
|
||||
show_avatars: true,
|
||||
|
@ -652,15 +702,15 @@ export async function saveUserSettingsBio(
|
|||
|
||||
export async function saveUserSettingsFederated(
|
||||
api: LemmyHttp,
|
||||
): Promise<LoginResponse> {
|
||||
let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png";
|
||||
let banner = "https://image.flaticon.com/icons/png/512/36/35896.png";
|
||||
): Promise<SuccessResponse> {
|
||||
let avatar = sampleImage;
|
||||
let banner = sampleImage;
|
||||
let bio = "a changed bio";
|
||||
let form: SaveUserSettings = {
|
||||
show_nsfw: false,
|
||||
blur_nsfw: true,
|
||||
auto_expand: false,
|
||||
default_sort_type: "Hot",
|
||||
default_post_sort_type: "Hot",
|
||||
default_listing_type: "All",
|
||||
interface_language: "",
|
||||
avatar,
|
||||
|
@ -676,7 +726,7 @@ export async function saveUserSettingsFederated(
|
|||
export async function saveUserSettings(
|
||||
api: LemmyHttp,
|
||||
form: SaveUserSettings,
|
||||
): Promise<LoginResponse> {
|
||||
): Promise<SuccessResponse> {
|
||||
return api.saveUserSettings(form);
|
||||
}
|
||||
export async function getPersonDetails(
|
||||
|
@ -689,9 +739,7 @@ export async function getPersonDetails(
|
|||
return api.getPersonDetails(form);
|
||||
}
|
||||
|
||||
export async function deleteUser(
|
||||
api: LemmyHttp,
|
||||
): Promise<DeleteAccountResponse> {
|
||||
export async function deleteUser(api: LemmyHttp): Promise<SuccessResponse> {
|
||||
let form: DeleteAccount = {
|
||||
delete_content: true,
|
||||
password,
|
||||
|
@ -722,6 +770,7 @@ export async function unfollowRemotes(
|
|||
await Promise.all(
|
||||
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
|
||||
);
|
||||
|
||||
let siteRes = await getSite(api);
|
||||
return siteRes;
|
||||
}
|
||||
|
@ -767,6 +816,18 @@ export async function reportComment(
|
|||
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(
|
||||
api: LemmyHttp,
|
||||
): Promise<ListCommentReportsResponse> {
|
||||
|
@ -777,9 +838,12 @@ export async function listCommentReports(
|
|||
export function getPosts(
|
||||
api: LemmyHttp,
|
||||
listingType?: ListingType,
|
||||
community_id?: number,
|
||||
): Promise<GetPostsResponse> {
|
||||
let form: GetPosts = {
|
||||
type_: listingType,
|
||||
limit: 50,
|
||||
community_id,
|
||||
};
|
||||
return api.getPosts(form);
|
||||
}
|
||||
|
@ -796,6 +860,18 @@ export function blockInstance(
|
|||
return api.blockInstance(form);
|
||||
}
|
||||
|
||||
export function blockCommunity(
|
||||
api: LemmyHttp,
|
||||
community_id: CommunityId,
|
||||
block: boolean,
|
||||
): Promise<BlockCommunityResponse> {
|
||||
let form: BlockCommunity = {
|
||||
community_id,
|
||||
block,
|
||||
};
|
||||
return api.blockCommunity(form);
|
||||
}
|
||||
|
||||
export function delay(millis = 500) {
|
||||
return new Promise(resolve => setTimeout(resolve, millis));
|
||||
}
|
||||
|
@ -819,13 +895,49 @@ export function randomString(length: number): string {
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function deleteAllImages(api: LemmyHttp) {
|
||||
const imagesRes = await api.listAllMedia({
|
||||
limit: imageFetchLimit,
|
||||
});
|
||||
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() {
|
||||
await Promise.all([
|
||||
unfollowRemotes(alpha),
|
||||
unfollowRemotes(beta),
|
||||
unfollowRemotes(gamma),
|
||||
unfollowRemotes(delta),
|
||||
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 {
|
||||
|
@ -836,6 +948,7 @@ export function getCommentParentId(comment: Comment): number | undefined {
|
|||
if (split.length > 1) {
|
||||
return Number(split[split.length - 2]);
|
||||
} else {
|
||||
console.log(`Failed to extract comment parent id from ${comment.path}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,19 +12,22 @@ import {
|
|||
createComment,
|
||||
resolveBetaCommunity,
|
||||
deleteUser,
|
||||
resolvePost,
|
||||
resolveComment,
|
||||
saveUserSettingsFederated,
|
||||
setupLogins,
|
||||
alphaUrl,
|
||||
saveUserSettings,
|
||||
getPost,
|
||||
getComments,
|
||||
fetchFunction,
|
||||
alphaImage,
|
||||
unfollows,
|
||||
saveUserSettingsBio,
|
||||
} from "./shared";
|
||||
import { LemmyHttp, SaveUserSettings } from "lemmy-js-client";
|
||||
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
|
||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
});
|
||||
beforeAll(setupLogins);
|
||||
afterAll(unfollows);
|
||||
|
||||
let apShortname: string;
|
||||
|
||||
|
@ -39,18 +42,14 @@ function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) {
|
|||
}
|
||||
|
||||
test("Create user", async () => {
|
||||
let userRes = await registerUser(alpha);
|
||||
expect(userRes.jwt).toBeDefined();
|
||||
let user = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: `Bearer ${userRes.jwt ?? ""}` },
|
||||
});
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
if (!site.my_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 () => {
|
||||
|
@ -70,14 +69,10 @@ test("Set some user settings, check that they are federated", async () => {
|
|||
});
|
||||
|
||||
test("Delete user", async () => {
|
||||
let userRes = await registerUser(alpha);
|
||||
expect(userRes.jwt).toBeDefined();
|
||||
let user = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: `Bearer ${userRes.jwt ?? ""}` },
|
||||
});
|
||||
let user = await registerUser(alpha, alphaUrl);
|
||||
|
||||
// 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;
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
|
@ -103,23 +98,28 @@ test("Delete user", async () => {
|
|||
|
||||
await deleteUser(user);
|
||||
|
||||
await expect(resolvePost(alpha, localPost)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
// check that posts and comments are marked as deleted on other instances.
|
||||
// use get methods to avoid refetching from origin instance
|
||||
expect((await getPost(alpha, localPost.id)).post_view.post.deleted).toBe(
|
||||
true,
|
||||
);
|
||||
await expect(resolveComment(alpha, localComment)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
);
|
||||
await expect(resolvePost(alpha, remotePost)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
);
|
||||
await expect(resolveComment(alpha, remoteComment)).rejects.toBe(
|
||||
"couldnt_find_object",
|
||||
expect((await getPost(alpha, remotePost.id)).post_view.post.deleted).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(await getComments(alpha, localComment.post_id)).comments[0].comment
|
||||
.deleted,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(await getComments(alpha, remoteComment.post_id)).comments[0].comment
|
||||
.deleted,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("Requests with invalid auth should be treated as unauthenticated", async () => {
|
||||
let invalid_auth = new LemmyHttp(alphaUrl, {
|
||||
headers: { Authorization: "Bearer foobar" },
|
||||
fetchFunction,
|
||||
});
|
||||
let site = await getSite(invalid_auth);
|
||||
expect(site.my_user).toBeUndefined();
|
||||
|
@ -129,3 +129,88 @@ test("Requests with invalid auth should be treated as unauthenticated", async ()
|
|||
let posts = invalid_auth.getPosts(form);
|
||||
expect((await posts).posts).toBeDefined();
|
||||
});
|
||||
|
||||
test("Create user with Arabic name", async () => {
|
||||
let user = await registerUser(
|
||||
alpha,
|
||||
alphaUrl,
|
||||
"تجريب" + Math.random().toString().slice(2, 10), // less than actor_name_max_length
|
||||
);
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
if (!site.my_user) {
|
||||
throw "Missing site user";
|
||||
}
|
||||
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
||||
|
||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
||||
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);
|
||||
});
|
||||
|
|
3074
api_tests/yarn.lock
3074
api_tests/yarn.lock
File diff suppressed because it is too large
Load diff
89
cliff.toml
Normal file
89
cliff.toml
Normal file
|
@ -0,0 +1,89 @@
|
|||
# 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"
|
|
@ -34,17 +34,49 @@
|
|||
# Name of the postgres database for lemmy
|
||||
database: "string"
|
||||
# Maximum number of active sql connections
|
||||
pool_size: 95
|
||||
pool_size: 30
|
||||
}
|
||||
# Settings related to activitypub federation
|
||||
# Pictrs image server configuration.
|
||||
pictrs: {
|
||||
# Address where pictrs is available (for image hosting)
|
||||
url: "http://localhost:8080/"
|
||||
# Set a custom pictrs API key. ( Required for deleting images )
|
||||
api_key: "string"
|
||||
# Cache remote images
|
||||
cache_remote_images: true
|
||||
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
|
||||
# equivalent to `image_mode: StoreLinkPreviews`.
|
||||
#
|
||||
# To be removed in 0.20
|
||||
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)
|
||||
upload_timeout: 30
|
||||
# Resize post thumbnails to this maximum width/height.
|
||||
max_thumbnail_size: 256
|
||||
}
|
||||
# Email sending configuration. All options except login/password are mandatory
|
||||
email: {
|
||||
|
@ -78,12 +110,17 @@
|
|||
port: 8536
|
||||
# Whether the site is available over TLS. Needs to be true for federation to work.
|
||||
tls_enabled: true
|
||||
# The number of activitypub federation workers that can be in-flight concurrently
|
||||
worker_count: 0
|
||||
# The number of activitypub federation retry workers that can be in-flight concurrently
|
||||
retry_count: 0
|
||||
federation: {
|
||||
# Limit to the number of concurrent outgoing federation requests per target instance.
|
||||
# Set this to a higher value than 1 (e.g. 6) only if you have a huge instance (>10 activities
|
||||
# per second) and if a receiving instance is not keeping up.
|
||||
concurrent_sends_per_instance: 1
|
||||
}
|
||||
prometheus: {
|
||||
bind: "127.0.0.1"
|
||||
port: 10002
|
||||
}
|
||||
# Sets a response Access-Control-Allow-Origin CORS header
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
||||
cors_origin: "*"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[package]
|
||||
name = "lemmy_api"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
|
@ -13,6 +14,9 @@ name = "lemmy_api"
|
|||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lemmy_utils = { workspace = true }
|
||||
lemmy_db_schema = { workspace = true, features = ["full"] }
|
||||
|
@ -22,22 +26,21 @@ lemmy_db_views_actor = { workspace = true, features = ["full"] }
|
|||
lemmy_api_common = { workspace = true, features = ["full"] }
|
||||
activitypub_federation = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
actix-web = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
captcha = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wav = "1.0.0"
|
||||
sitemap-rs = "0.2.0"
|
||||
totp-rs = { version = "5.0.2", features = ["gen_secret", "otpauth"] }
|
||||
actix-web-httpauth = "0.8.1"
|
||||
hound = "3.5.1"
|
||||
sitemap-rs = "0.2.1"
|
||||
totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] }
|
||||
actix-web-httpauth = "0.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
elementtree = "1.2.3"
|
||||
pretty_assertions = { workspace = true }
|
||||
lemmy_api_crud = { workspace = true }
|
||||
|
|
|
@ -9,15 +9,20 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn distinguish_comment(
|
||||
data: Json<DistinguishComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentResponse>, LemmyError> {
|
||||
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None).await?;
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
@ -26,6 +31,11 @@ pub async fn distinguish_comment(
|
|||
)
|
||||
.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
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
|
@ -47,7 +57,7 @@ pub async fn distinguish_comment(
|
|||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(local_user_view.person.id),
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use lemmy_api_common::{
|
|||
comment::{CommentResponse, CreateCommentLike},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_user_action, check_downvotes_enabled},
|
||||
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::LocalUserId,
|
||||
|
@ -17,7 +17,7 @@ use lemmy_db_schema::{
|
|||
traits::Likeable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -25,16 +25,28 @@ pub async fn like_comment(
|
|||
data: Json<CreateCommentLike>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
let comment_id = data.comment_id;
|
||||
|
||||
let mut recipient_ids = Vec::<LocalUserId>::new();
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
check_downvotes_enabled(data.score, &local_site)?;
|
||||
check_local_vote_mode(
|
||||
data.score,
|
||||
VoteItem::Comment(comment_id),
|
||||
&local_site,
|
||||
local_user_view.person.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
check_bot_account(&local_user_view.person)?;
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
@ -45,7 +57,7 @@ pub async fn like_comment(
|
|||
|
||||
// Add parent poster or commenter to recipients
|
||||
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
|
||||
if let Ok(reply) = comment_reply {
|
||||
if let Ok(Some(reply)) = comment_reply {
|
||||
let recipient_id = reply.recipient_id;
|
||||
if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await
|
||||
{
|
||||
|
@ -55,7 +67,6 @@ pub async fn like_comment(
|
|||
|
||||
let like_form = CommentLikeForm {
|
||||
comment_id: data.comment_id,
|
||||
post_id: orig_comment.post.id,
|
||||
person_id: local_user_view.person.id,
|
||||
score: data.score,
|
||||
};
|
||||
|
@ -74,12 +85,12 @@ pub async fn like_comment(
|
|||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LikePostOrComment(
|
||||
orig_comment.comment.ap_id,
|
||||
local_user_view.person.clone(),
|
||||
orig_comment.community,
|
||||
data.score,
|
||||
),
|
||||
SendActivityData::LikePostOrComment {
|
||||
object_id: orig_comment.comment.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community: orig_comment.community,
|
||||
score: data.score,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
35
crates/api/src/comment/list_comment_likes.rs
Normal file
35
crates/api/src/comment/list_comment_likes.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
|
||||
/// 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.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
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 }))
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod distinguish;
|
||||
pub mod like;
|
||||
pub mod list_comment_likes;
|
||||
pub mod save;
|
||||
|
|
|
@ -8,14 +8,14 @@ use lemmy_db_schema::{
|
|||
traits::Saveable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_comment(
|
||||
data: Json<SaveComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let comment_saved_form = CommentSavedForm {
|
||||
comment_id: data.comment_id,
|
||||
person_id: local_user_view.person.id,
|
||||
|
@ -32,8 +32,12 @@ pub async fn save_comment(
|
|||
}
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?;
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(CommentResponse {
|
||||
comment_view,
|
||||
|
|
|
@ -5,7 +5,11 @@ use lemmy_api_common::{
|
|||
comment::{CommentReportResponse, CreateCommentReport},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_user_action, send_new_report_email_to_admins},
|
||||
utils::{
|
||||
check_comment_deleted_or_removed,
|
||||
check_community_user_action,
|
||||
send_new_report_email_to_admins,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -15,7 +19,7 @@ use lemmy_db_schema::{
|
|||
traits::Reportable,
|
||||
};
|
||||
use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Creates a comment report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -23,7 +27,7 @@ pub async fn create_comment_report(
|
|||
data: Json<CreateCommentReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
|
@ -31,7 +35,12 @@ pub async fn create_comment_report(
|
|||
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_id = data.comment_id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?;
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
@ -40,6 +49,9 @@ pub async fn create_comment_report(
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Don't allow creating reports for removed / deleted comments
|
||||
check_comment_deleted_or_removed(&comment_view.comment)?;
|
||||
|
||||
let report_form = CommentReportForm {
|
||||
creator_id: person_id,
|
||||
comment_id,
|
||||
|
@ -66,12 +78,12 @@ pub async fn create_comment_report(
|
|||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::CreateReport(
|
||||
comment_view.comment.ap_id.inner().clone(),
|
||||
local_user_view.person,
|
||||
comment_view.community,
|
||||
data.reason.clone(),
|
||||
),
|
||||
SendActivityData::CreateReport {
|
||||
object_id: comment_view.comment.ap_id.inner().clone(),
|
||||
actor: local_user_view.person,
|
||||
community: comment_view.community,
|
||||
reason: data.reason.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -2,10 +2,10 @@ use actix_web::web::{Data, Json, Query};
|
|||
use lemmy_api_common::{
|
||||
comment::{ListCommentReports, ListCommentReportsResponse},
|
||||
context::LemmyContext,
|
||||
utils::check_community_mod_action_opt,
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Lists comment reports for a community if an id is supplied
|
||||
/// or returns all comment reports for communities a user moderates
|
||||
|
@ -14,16 +14,18 @@ pub async fn list_comment_reports(
|
|||
data: Query<ListCommentReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<ListCommentReportsResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<ListCommentReportsResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let comment_id = data.comment_id;
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
|
||||
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let comment_reports = CommentReportQuery {
|
||||
community_id,
|
||||
comment_id,
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
|
|
|
@ -6,7 +6,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{CommentReportView, LocalUserView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Resolves or unresolves a comment report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -14,7 +14,7 @@ pub async fn resolve_comment_report(
|
|||
data: Json<ResolveCommentReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
||||
let report_id = data.report_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
|
||||
|
@ -23,7 +23,7 @@ pub async fn resolve_comment_report(
|
|||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
report.community.id,
|
||||
false,
|
||||
true,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -9,20 +9,21 @@ use lemmy_api_common::{
|
|||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
||||
local_user::LocalUser,
|
||||
moderator::{ModAddCommunity, ModAddCommunityForm},
|
||||
},
|
||||
traits::{Crud, Joinable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn add_mod_to_community(
|
||||
data: Json<AddModToCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<AddModToCommunityResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<AddModToCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
|
||||
// Verify that only mods or admins can add mod
|
||||
|
@ -33,9 +34,30 @@ pub async fn add_mod_to_community(
|
|||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If its a mod removal, also check that you're a higher mod.
|
||||
if !data.added {
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let community = Community::read(&mut context.pool(), community_id).await?;
|
||||
|
||||
// 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 {
|
||||
Err(LemmyErrorType::NotAModerator)?
|
||||
CommunityModeratorView::check_is_community_moderator(
|
||||
&mut context.pool(),
|
||||
community.id,
|
||||
local_user_view.person.id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Update in local database
|
||||
|
@ -69,12 +91,12 @@ pub async fn add_mod_to_community(
|
|||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::AddModToCommunity(
|
||||
local_user_view.person,
|
||||
data.community_id,
|
||||
data.person_id,
|
||||
data.added,
|
||||
),
|
||||
SendActivityData::AddModToCommunity {
|
||||
moderator: local_user_view.person,
|
||||
community_id: data.community_id,
|
||||
target: data.person_id,
|
||||
added: data.added,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -4,7 +4,11 @@ use lemmy_api_common::{
|
|||
community::{BanFromCommunity, BanFromCommunityResponse},
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community},
|
||||
utils::{
|
||||
check_community_mod_action,
|
||||
check_expire_time,
|
||||
remove_or_restore_user_data_in_community,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -14,6 +18,7 @@ use lemmy_db_schema::{
|
|||
CommunityPersonBan,
|
||||
CommunityPersonBanForm,
|
||||
},
|
||||
local_user::LocalUser,
|
||||
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
||||
},
|
||||
traits::{Bannable, Crud, Followable},
|
||||
|
@ -21,7 +26,7 @@ use lemmy_db_schema::{
|
|||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::validation::is_valid_body_field,
|
||||
};
|
||||
|
||||
|
@ -30,9 +35,8 @@ pub async fn ban_from_community(
|
|||
data: Json<BanFromCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<BanFromCommunityResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BanFromCommunityResponse>> {
|
||||
let banned_person_id = data.person_id;
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
// Verify that only mods or admins can ban
|
||||
|
@ -43,7 +47,18 @@ pub async fn ban_from_community(
|
|||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
data.community_id,
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
||||
let community_user_ban_form = CommunityPersonBanForm {
|
||||
community_id: data.community_id,
|
||||
|
@ -73,9 +88,18 @@ pub async fn ban_from_community(
|
|||
}
|
||||
|
||||
// Remove/Restore their data if that's desired
|
||||
if remove_data {
|
||||
remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?;
|
||||
}
|
||||
if data.remove_or_restore_data.unwrap_or(false) {
|
||||
let remove_data = data.ban;
|
||||
remove_or_restore_user_data_in_community(
|
||||
data.community_id,
|
||||
local_user_view.person.id,
|
||||
banned_person_id,
|
||||
remove_data,
|
||||
&data.reason,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanFromCommunityForm {
|
||||
|
@ -92,12 +116,12 @@ pub async fn ban_from_community(
|
|||
let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromCommunity(
|
||||
local_user_view.person,
|
||||
data.community_id,
|
||||
person_view.person.clone(),
|
||||
data.0.clone(),
|
||||
),
|
||||
SendActivityData::BanFromCommunity {
|
||||
moderator: local_user_view.person,
|
||||
community_id: data.community_id,
|
||||
target: person_view.person.clone(),
|
||||
data: data.0.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -14,14 +14,14 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn block_community(
|
||||
data: Json<BlockCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<BlockCommunityResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BlockCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_block_form = CommunityBlockForm {
|
||||
|
@ -50,8 +50,13 @@ pub async fn block_community(
|
|||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
||||
}
|
||||
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FollowCommunity(
|
||||
|
|
|
@ -15,14 +15,14 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn follow_community(
|
||||
data: Json<FollowCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommunityResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommunityResponse>> {
|
||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
||||
let mut community_follower_form = CommunityFollowerForm {
|
||||
community_id: community.id,
|
||||
|
@ -45,23 +45,29 @@ pub async fn follow_community(
|
|||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
||||
}
|
||||
}
|
||||
if !data.follow {
|
||||
} else {
|
||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
||||
}
|
||||
|
||||
if !community.local {
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
||||
|
||||
Ok(Json(CommunityResponse {
|
||||
|
|
|
@ -15,14 +15,14 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn hide_community(
|
||||
data: Json<HideCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Verify its a admin (only admin can hide or unhide it)
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
|
|
|
@ -3,4 +3,5 @@ pub mod ban;
|
|||
pub mod block;
|
||||
pub mod follow;
|
||||
pub mod hide;
|
||||
pub mod random;
|
||||
pub mod transfer;
|
||||
|
|
55
crates/api/src/community/random.rs
Normal file
55
crates/api/src/community/random.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::{Json, Query};
|
||||
use lemmy_api_common::{
|
||||
community::{CommunityResponse, GetRandomCommunity},
|
||||
context::LemmyContext,
|
||||
utils::{check_private_instance, is_mod_or_admin_opt},
|
||||
};
|
||||
use lemmy_db_schema::source::{
|
||||
actor_language::CommunityLanguage,
|
||||
community::Community,
|
||||
local_site::LocalSite,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_random_community(
|
||||
data: Query<GetRandomCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> LemmyResult<Json<CommunityResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
||||
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
|
||||
|
||||
let random_community_id =
|
||||
Community::get_random_community_id(&mut context.pool(), &data.type_).await?;
|
||||
|
||||
let is_mod_or_admin = is_mod_or_admin_opt(
|
||||
&mut context.pool(),
|
||||
local_user_view.as_ref(),
|
||||
Some(random_community_id),
|
||||
)
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
random_community_id,
|
||||
local_user,
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let discussion_languages =
|
||||
CommunityLanguage::read(&mut context.pool(), random_community_id).await?;
|
||||
|
||||
Ok(Json(CommunityResponse {
|
||||
community_view,
|
||||
discussion_languages,
|
||||
}))
|
||||
}
|
|
@ -15,7 +15,7 @@ use lemmy_db_schema::{
|
|||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
location_info,
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,7 @@ pub async fn transfer_community(
|
|||
data: Json<TransferCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetCommunityResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetCommunityResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let mut community_mods =
|
||||
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
@ -76,16 +76,16 @@ pub async fn transfer_community(
|
|||
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
// Return the jwt
|
||||
Ok(Json(GetCommunityResponse {
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
use activitypub_federation::config::Data;
|
||||
use actix_web::{http::header::Header, HttpRequest};
|
||||
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
|
||||
use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
|
||||
use captcha::Captcha;
|
||||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
community::BanFromCommunity,
|
||||
context::LemmyContext,
|
||||
utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
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_utils::{
|
||||
error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
||||
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
||||
utils::slurs::check_slurs,
|
||||
};
|
||||
use std::io::Cursor;
|
||||
|
@ -28,31 +44,37 @@ pub mod site;
|
|||
pub mod sitemap;
|
||||
|
||||
/// Converts the captcha to a base64 encoded wav audio file
|
||||
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result<String, LemmyError> {
|
||||
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
|
||||
let letters = captcha.as_wav();
|
||||
|
||||
// Decode each wav file, concatenate the samples
|
||||
let mut concat_samples: Vec<i16> = Vec::new();
|
||||
let mut any_header: Option<wav::Header> = None;
|
||||
let mut any_header: Option<hound::WavSpec> = None;
|
||||
for letter in letters {
|
||||
let mut cursor = Cursor::new(letter.unwrap_or_default());
|
||||
let (header, samples) = wav::read(&mut cursor)?;
|
||||
any_header = Some(header);
|
||||
if let Some(samples16) = samples.as_sixteen() {
|
||||
let reader = hound::WavReader::new(&mut cursor)?;
|
||||
any_header = Some(reader.spec());
|
||||
let samples16 = reader
|
||||
.into_samples::<i16>()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
concat_samples.extend(samples16);
|
||||
} else {
|
||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the concatenated result as a wav file
|
||||
let mut output_buffer = Cursor::new(vec![]);
|
||||
if let Some(header) = any_header {
|
||||
wav::write(
|
||||
header,
|
||||
&wav::BitDepth::Sixteen(concat_samples),
|
||||
&mut output_buffer,
|
||||
)
|
||||
let mut writer = hound::WavWriter::new(&mut output_buffer, header)
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
let mut writer16 = writer.get_i16_writer(concat_samples.len() as u32);
|
||||
for sample in concat_samples {
|
||||
writer16.write_sample(sample);
|
||||
}
|
||||
writer16
|
||||
.flush()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
writer
|
||||
.finalize()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
|
||||
Ok(base64.encode(output_buffer.into_inner()))
|
||||
|
@ -62,7 +84,7 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result<String, LemmyEr
|
|||
}
|
||||
|
||||
/// Check size of report
|
||||
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
|
||||
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> LemmyResult<()> {
|
||||
let slur_regex = &local_site_to_slur_regex(local_site);
|
||||
|
||||
check_slurs(reason, slur_regex)?;
|
||||
|
@ -75,22 +97,15 @@ pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Resul
|
|||
}
|
||||
}
|
||||
|
||||
pub fn read_auth_token(req: &HttpRequest) -> Result<Option<String>, LemmyError> {
|
||||
pub fn read_auth_token(req: &HttpRequest) -> LemmyResult<Option<String>> {
|
||||
// Try reading jwt from auth header
|
||||
if let Ok(header) = Authorization::<Bearer>::parse(req) {
|
||||
Ok(Some(header.as_ref().token().to_string()))
|
||||
}
|
||||
// If that fails, try to read from cookie
|
||||
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();
|
||||
if !secure || !http_only {
|
||||
Err(LemmyError::from(LemmyErrorType::AuthCookieInsecure))
|
||||
} else {
|
||||
Ok(Some(cookie.value().to_string()))
|
||||
}
|
||||
}
|
||||
// Otherwise, there's no auth
|
||||
else {
|
||||
Ok(None)
|
||||
|
@ -126,11 +141,7 @@ pub(crate) fn generate_totp_2fa_secret() -> String {
|
|||
Secret::generate_secret().to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn build_totp_2fa(
|
||||
site_name: &str,
|
||||
username: &str,
|
||||
secret: &str,
|
||||
) -> Result<TOTP, LemmyError> {
|
||||
fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<TOTP> {
|
||||
let sec = Secret::Raw(secret.as_bytes().to_vec());
|
||||
let sec_bytes = sec
|
||||
.to_bytes()
|
||||
|
@ -142,17 +153,108 @@ pub(crate) fn build_totp_2fa(
|
|||
1,
|
||||
30,
|
||||
sec_bytes,
|
||||
Some(site_name.to_string()),
|
||||
Some(hostname.to_string()),
|
||||
username.to_string(),
|
||||
)
|
||||
.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_or_restore_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_or_restore_data: *remove_or_restore_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)]
|
||||
pub async fn local_user_view_from_jwt(
|
||||
jwt: &str,
|
||||
context: &LemmyContext,
|
||||
) -> Result<LocalUserView, LemmyError> {
|
||||
) -> LemmyResult<LocalUserView> {
|
||||
let local_user_id = Claims::validate(jwt, context)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
|
||||
|
@ -164,15 +266,13 @@ pub async fn local_user_view_from_jwt(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_totp() {
|
||||
let generated_secret = generate_totp_2fa_secret();
|
||||
let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
|
||||
let totp = build_totp_2fa("lemmy.ml", "my_name", &generated_secret);
|
||||
assert!(totp.is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,23 +13,33 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn add_admin(
|
||||
data: Json<AddAdmin>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<AddAdminResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<AddAdminResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// If its an admin removal, also check that you're a higher admin
|
||||
if !data.added {
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Make sure that the person_id added is local
|
||||
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::ObjectNotLocal)?;
|
||||
.map_err(|_| LemmyErrorType::ObjectNotLocal)?;
|
||||
|
||||
let added_admin = LocalUser::update(
|
||||
LocalUser::update(
|
||||
&mut context.pool(),
|
||||
added_local_user.local_user.id,
|
||||
&LocalUserUpdateForm {
|
||||
|
@ -43,7 +53,7 @@ pub async fn add_admin(
|
|||
// Mod tables
|
||||
let form = ModAddForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: added_admin.person_id,
|
||||
other_person_id: added_local_user.person.id,
|
||||
removed: Some(!data.added),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use crate::ban_nonlocal_user_from_local_communities;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{BanPerson, BanPersonResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_expire_time, is_admin, remove_user_data},
|
||||
utils::{check_expire_time, is_admin, remove_or_restore_user_data},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
login_token::LoginToken,
|
||||
moderator::{ModBan, ModBanForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
|
@ -17,7 +19,7 @@ use lemmy_db_schema::{
|
|||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::validation::is_valid_body_field,
|
||||
};
|
||||
|
||||
|
@ -26,11 +28,21 @@ pub async fn ban_from_site(
|
|||
data: Json<BanPerson>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<BanPersonResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BanPersonResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
// Also make sure you're a higher admin than the target
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
|
@ -46,22 +58,29 @@ pub async fn ban_from_site(
|
|||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
||||
|
||||
let local_user_id = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
.local_user
|
||||
.id;
|
||||
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;
|
||||
// if its a local user, invalidate logins
|
||||
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
|
||||
if let Ok(local_user) = local_user {
|
||||
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
|
||||
}
|
||||
|
||||
// Remove their data if that's desired
|
||||
let remove_data = data.remove_data.unwrap_or(false);
|
||||
if remove_data {
|
||||
remove_user_data(person.id, &context).await?;
|
||||
}
|
||||
if data.remove_or_restore_data.unwrap_or(false) {
|
||||
let removed = data.ban;
|
||||
remove_or_restore_user_data(
|
||||
local_user_view.person.id,
|
||||
person.id,
|
||||
removed,
|
||||
&data.reason,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
// Mod tables
|
||||
let form = ModBanForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: data.person_id,
|
||||
other_person_id: person.id,
|
||||
reason: data.reason.clone(),
|
||||
banned: Some(data.ban),
|
||||
expires,
|
||||
|
@ -69,14 +88,28 @@ pub async fn ban_from_site(
|
|||
|
||||
ModBan::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
|
||||
let person_view = PersonView::read(&mut context.pool(), person.id).await?;
|
||||
|
||||
ban_nonlocal_user_from_local_communities(
|
||||
&local_user_view,
|
||||
&person,
|
||||
data.ban,
|
||||
&data.reason,
|
||||
&data.remove_or_restore_data,
|
||||
&data.expires,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromSite(
|
||||
local_user_view.person,
|
||||
person_view.person.clone(),
|
||||
data.0.clone(),
|
||||
),
|
||||
SendActivityData::BanFromSite {
|
||||
moderator: local_user_view.person,
|
||||
banned_user: person_view.person.clone(),
|
||||
reason: data.reason.clone(),
|
||||
remove_or_restore_data: data.remove_or_restore_data,
|
||||
ban: data.ban,
|
||||
expires: data.expires,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -9,14 +9,14 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn block_person(
|
||||
data: Json<BlockPerson>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<BlockPersonResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BlockPersonResponse>> {
|
||||
let target_id = data.person_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
|
@ -30,8 +30,11 @@ pub async fn block_person(
|
|||
target_id,
|
||||
};
|
||||
|
||||
let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await;
|
||||
if target_user.map(|t| t.local_user.admin) == Ok(true) {
|
||||
let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
if target_user.is_some_and(|t| t.local_user.admin) {
|
||||
Err(LemmyErrorType::CantBlockAdmin)?
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn change_password(
|
||||
|
@ -19,7 +19,7 @@ pub async fn change_password(
|
|||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<LoginResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
password_length_check(&data.new_password)?;
|
||||
|
||||
// Make sure passwords match
|
||||
|
@ -28,11 +28,13 @@ pub async fn change_password(
|
|||
}
|
||||
|
||||
// Check the old password
|
||||
let valid: bool = verify(
|
||||
&data.old_password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
.unwrap_or(false);
|
||||
let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted
|
||||
{
|
||||
verify(&data.old_password, password_encrypted).unwrap_or(false)
|
||||
} else {
|
||||
data.old_password.is_empty()
|
||||
};
|
||||
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
|
|
|
@ -10,18 +10,18 @@ use lemmy_db_schema::source::{
|
|||
login_token::LoginToken,
|
||||
password_reset_request::PasswordResetRequest,
|
||||
};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn change_password_after_reset(
|
||||
data: Json<PasswordChangeAfterReset>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Fetch the user_id from the token
|
||||
let token = data.token.clone();
|
||||
let local_user_id = PasswordResetRequest::read_from_token(&mut context.pool(), &token)
|
||||
.await
|
||||
.map(|p| p.local_user_id)?;
|
||||
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
|
||||
.await?
|
||||
.local_user_id;
|
||||
|
||||
password_length_check(&data.password)?;
|
||||
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
use crate::{build_totp_2fa, generate_totp_2fa_secret};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::GenerateTotpSecretResponse,
|
||||
sensitive::Sensitive,
|
||||
use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
|
||||
use lemmy_db_schema::source::{
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
site::Site,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::local_user::{LocalUser, LocalUserUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
/// 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.
|
||||
|
@ -19,16 +15,15 @@ use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
|||
pub async fn generate_totp_secret(
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<GenerateTotpSecretResponse>, LemmyError> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
|
||||
let site = Site::read_local(&mut context.pool()).await?;
|
||||
|
||||
if local_user_view.local_user.totp_2fa_enabled {
|
||||
return Err(LemmyErrorType::TotpAlreadyEnabled)?;
|
||||
}
|
||||
|
||||
let secret = generate_totp_2fa_secret();
|
||||
let secret_url =
|
||||
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
|
||||
let secret_url = build_totp_2fa(&site.name, &local_user_view.person.name, &secret)?.get_url();
|
||||
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
totp_2fa_secret: Some(Some(secret)),
|
||||
|
@ -42,6 +37,6 @@ pub async fn generate_totp_secret(
|
|||
.await?;
|
||||
|
||||
Ok(Json(GenerateTotpSecretResponse {
|
||||
totp_secret_url: Sensitive::new(secret_url),
|
||||
totp_secret_url: secret_url.into(),
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
use crate::captcha_as_wav_base64;
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::{
|
||||
http::{
|
||||
header::{CacheControl, CacheDirective},
|
||||
StatusCode,
|
||||
},
|
||||
web::{Data, Json},
|
||||
HttpResponse,
|
||||
HttpResponseBuilder,
|
||||
};
|
||||
use captcha::{gen, Difficulty};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
|
@ -9,16 +17,16 @@ use lemmy_db_schema::source::{
|
|||
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
|
||||
local_site::LocalSite,
|
||||
};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_captcha(
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<GetCaptchaResponse>, LemmyError> {
|
||||
pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpResponse> {
|
||||
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 {
|
||||
return Ok(Json(GetCaptchaResponse { ok: None }));
|
||||
return Ok(res.json(Json(GetCaptchaResponse { ok: None })));
|
||||
}
|
||||
|
||||
let captcha = gen(match local_site.captcha_difficulty.as_str() {
|
||||
|
@ -37,11 +45,12 @@ pub async fn get_captcha(
|
|||
// Stores the captcha item in the db
|
||||
let captcha = CaptchaAnswer::insert(&mut context.pool(), &captcha_form).await?;
|
||||
|
||||
Ok(Json(GetCaptchaResponse {
|
||||
let json = Json(GetCaptchaResponse {
|
||||
ok: Some(CaptchaResponse {
|
||||
png,
|
||||
wav,
|
||||
uuid: captcha.uuid.to_string(),
|
||||
}),
|
||||
}))
|
||||
});
|
||||
Ok(res.json(json))
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ use actix_web::web::{Data, Json};
|
|||
use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
pub async fn list_banned_users(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<BannedPersonsResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BannedPersonsResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_api_common::{context::LemmyContext, person::ListLoginsResponse};
|
||||
use lemmy_db_schema::source::login_token::LoginToken;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
pub async fn list_logins(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<Vec<LoginToken>>, LemmyError> {
|
||||
) -> LemmyResult<Json<ListLoginsResponse>> {
|
||||
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
|
||||
|
||||
Ok(Json(logins))
|
||||
Ok(Json(ListLoginsResponse { logins }))
|
||||
}
|
||||
|
|
25
crates/api/src/local_user/list_media.rs
Normal file
25
crates/api/src/local_user/list_media.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
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 }))
|
||||
}
|
|
@ -1,100 +1,61 @@
|
|||
use crate::check_totp_2fa_valid;
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
web::{Data, Json},
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
};
|
||||
use bcrypt::verify;
|
||||
use lemmy_api_common::{
|
||||
claims::Claims,
|
||||
context::LemmyContext,
|
||||
person::{Login, LoginResponse},
|
||||
utils::{check_user_valid, create_login_cookie},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{local_site::LocalSite, registration_application::RegistrationApplication},
|
||||
utils::DbPool,
|
||||
RegistrationMode,
|
||||
utils::{check_email_verified, check_registration_application, check_user_valid},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn login(
|
||||
data: Json<Login>,
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
) -> LemmyResult<Json<LoginResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
// Fetch that username / email
|
||||
let username_or_email = data.username_or_email.clone();
|
||||
let local_user_view =
|
||||
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
|
||||
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email).await?;
|
||||
|
||||
// Verify the password
|
||||
let valid: bool = verify(
|
||||
&data.password,
|
||||
&local_user_view.local_user.password_encrypted,
|
||||
)
|
||||
let valid: bool = local_user_view
|
||||
.local_user
|
||||
.password_encrypted
|
||||
.as_ref()
|
||||
.and_then(|password_encrypted| verify(&data.password, password_encrypted).ok())
|
||||
.unwrap_or(false);
|
||||
if !valid {
|
||||
Err(LemmyErrorType::IncorrectLogin)?
|
||||
}
|
||||
check_user_valid(&local_user_view.person)?;
|
||||
|
||||
// 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_email_verified(&local_user_view, &site_view)?;
|
||||
|
||||
check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool())
|
||||
.await?;
|
||||
|
||||
// Check the totp if enabled
|
||||
if local_user_view.local_user.totp_2fa_enabled {
|
||||
check_totp_2fa_valid(&local_user_view, &data.totp_2fa_token, &site_view.site.name)?;
|
||||
check_totp_2fa_valid(
|
||||
&local_user_view,
|
||||
&data.totp_2fa_token,
|
||||
&context.settings().hostname,
|
||||
)?;
|
||||
}
|
||||
|
||||
let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?;
|
||||
|
||||
let json = LoginResponse {
|
||||
Ok(Json(LoginResponse {
|
||||
jwt: Some(jwt.clone()),
|
||||
verify_email_sent: false,
|
||||
registration_created: false,
|
||||
};
|
||||
|
||||
let mut res = HttpResponse::build(StatusCode::OK).json(json);
|
||||
res.add_cookie(&create_login_cookie(jwt))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn check_registration_application(
|
||||
local_user_view: &LocalUserView,
|
||||
local_site: &LocalSite,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> Result<(), LemmyError> {
|
||||
if (local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
|| local_site.registration_mode == RegistrationMode::Closed)
|
||||
&& !local_user_view.local_user.accepted_application
|
||||
&& !local_user_view.local_user.admin
|
||||
{
|
||||
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
|
||||
// was processed (either accepted or denied).
|
||||
let local_user_id = local_user_view.local_user.id;
|
||||
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
|
||||
if registration.admin_id.is_some() {
|
||||
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
|
||||
} else {
|
||||
Err(LemmyErrorType::RegistrationApplicationIsPending)?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::read_auth_token;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse};
|
||||
use lemmy_api_common::{context::LemmyContext, utils::AUTH_COOKIE_NAME};
|
||||
use lemmy_api_common::{context::LemmyContext, utils::AUTH_COOKIE_NAME, SuccessResponse};
|
||||
use lemmy_db_schema::source::login_token::LoginToken;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
@ -16,7 +16,7 @@ pub async fn logout(
|
|||
let jwt = read_auth_token(&req)?.ok_or(LemmyErrorType::NotLoggedIn)?;
|
||||
LoginToken::invalidate(&mut context.pool(), &jwt).await?;
|
||||
|
||||
let mut res = HttpResponse::Ok().finish();
|
||||
let mut res = HttpResponse::Ok().json(SuccessResponse::default());
|
||||
let cookie = Cookie::new(AUTH_COOKIE_NAME, "");
|
||||
res.add_removal_cookie(&cookie)?;
|
||||
Ok(res)
|
||||
|
|
|
@ -7,6 +7,7 @@ pub mod generate_totp_secret;
|
|||
pub mod get_captcha;
|
||||
pub mod list_banned;
|
||||
pub mod list_logins;
|
||||
pub mod list_media;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod notifications;
|
||||
|
|
|
@ -5,14 +5,14 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::person_mention_view::PersonMentionQuery;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_mentions(
|
||||
data: Query<GetPersonMentions>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetPersonMentionsResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetPersonMentionsResponse>> {
|
||||
let sort = data.sort;
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
|
|
|
@ -5,14 +5,14 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_replies(
|
||||
data: Query<GetReplies>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetRepliesResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
||||
let sort = data.sort;
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
|
|
|
@ -6,13 +6,13 @@ use lemmy_db_schema::source::{
|
|||
private_message::PrivateMessage,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_all_notifications_read(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetRepliesResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// Mark all comment_replies as read
|
||||
|
|
|
@ -9,14 +9,14 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::PersonMentionView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_person_mention_as_read(
|
||||
data: Json<MarkPersonMentionAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PersonMentionResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PersonMentionResponse>> {
|
||||
let person_mention_id = data.person_mention_id;
|
||||
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?;
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommentReplyView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_reply_as_read(
|
||||
data: Json<MarkCommentReplyAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<CommentReplyResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<CommentReplyResponse>> {
|
||||
let comment_reply_id = data.comment_reply_id;
|
||||
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
|
||||
|
||||
|
|
|
@ -2,18 +2,21 @@ use actix_web::web::{Data, Json};
|
|||
use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
|
||||
use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn unread_count(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetUnreadCountResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetUnreadCountResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
let replies = CommentReplyView::get_unread_replies(&mut context.pool(), person_id).await?;
|
||||
let replies =
|
||||
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?;
|
||||
|
||||
let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?;
|
||||
let mentions =
|
||||
PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user)
|
||||
.await?;
|
||||
|
||||
let private_messages =
|
||||
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::{GetReportCount, GetReportCountResponse},
|
||||
utils::check_community_mod_action_opt,
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::structs::{
|
||||
CommentReportView,
|
||||
|
@ -10,19 +10,19 @@ use lemmy_db_views::structs::{
|
|||
PostReportView,
|
||||
PrivateMessageReportView,
|
||||
};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn report_count(
|
||||
data: Json<GetReportCount>,
|
||||
data: Query<GetReportCount>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetReportCountResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetReportCountResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
let admin = local_user_view.local_user.admin;
|
||||
let community_id = data.community_id;
|
||||
|
||||
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let comment_reports =
|
||||
CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id)
|
||||
|
|
|
@ -2,12 +2,11 @@ use actix_web::web::{Data, Json};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::PasswordReset,
|
||||
utils::send_password_reset_email,
|
||||
utils::{check_email_verified, send_password_reset_email},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn reset_password(
|
||||
|
@ -18,17 +17,10 @@ pub async fn reset_password(
|
|||
let email = data.email.to_lowercase();
|
||||
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
|
||||
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
// Check for too many attempts (to limit potential abuse)
|
||||
let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count(
|
||||
&mut context.pool(),
|
||||
local_user_view.local_user.id,
|
||||
)
|
||||
.await?;
|
||||
if recent_resets_count >= 3 {
|
||||
Err(LemmyErrorType::PasswordResetLimitReached)?
|
||||
}
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
check_email_verified(&local_user_view, &site_view)?;
|
||||
|
||||
// Email the pure token to the user.
|
||||
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?;
|
||||
|
|
|
@ -1,48 +1,69 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::SaveUserSettings,
|
||||
utils::send_verification_email,
|
||||
request::replace_image,
|
||||
utils::{
|
||||
get_url_blocklist,
|
||||
local_site_to_slur_regex,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_api,
|
||||
send_verification_email,
|
||||
},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
actor_language::LocalUserLanguage,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{diesel_option_overwrite, diesel_option_overwrite_to_url},
|
||||
utils::{diesel_string_update, diesel_url_update},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorType},
|
||||
error::{LemmyErrorType, LemmyResult},
|
||||
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_user_settings(
|
||||
data: Json<SaveUserSettings>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
|
||||
let banner = diesel_option_overwrite_to_url(&data.banner)?;
|
||||
let bio = diesel_option_overwrite(data.bio.clone());
|
||||
let display_name = diesel_option_overwrite(data.display_name.clone());
|
||||
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
|
||||
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let bio = diesel_string_update(
|
||||
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context)
|
||||
.await?
|
||||
.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 = diesel_option_overwrite(email_deref.clone());
|
||||
let email = diesel_string_update(email_deref.as_deref());
|
||||
|
||||
if let Some(Some(email)) = &email {
|
||||
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 &previous_email != email {
|
||||
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
|
||||
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
||||
}
|
||||
if previous_email.deref() != email {
|
||||
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
|
||||
send_verification_email(
|
||||
&local_user_view,
|
||||
email,
|
||||
|
@ -53,7 +74,8 @@ pub async fn save_user_settings(
|
|||
}
|
||||
}
|
||||
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None
|
||||
// value
|
||||
if let Some(email) = &email {
|
||||
if email.is_none() && site_view.local_site.require_email_verification {
|
||||
Err(LemmyErrorType::EmailRequired)?
|
||||
|
@ -78,7 +100,8 @@ pub async fn save_user_settings(
|
|||
let local_user_id = local_user_view.local_user.id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let default_listing_type = data.default_listing_type;
|
||||
let default_sort_type = data.default_sort_type;
|
||||
let default_post_sort_type = data.default_post_sort_type;
|
||||
let default_comment_sort_type = data.default_comment_sort_type;
|
||||
|
||||
let person_form = PersonUpdateForm {
|
||||
display_name,
|
||||
|
@ -107,10 +130,9 @@ pub async fn save_user_settings(
|
|||
send_notifications_to_email: data.send_notifications_to_email,
|
||||
show_nsfw: data.show_nsfw,
|
||||
blur_nsfw: data.blur_nsfw,
|
||||
auto_expand: data.auto_expand,
|
||||
show_bot_accounts: data.show_bot_accounts,
|
||||
show_scores: data.show_scores,
|
||||
default_sort_type,
|
||||
default_post_sort_type,
|
||||
default_comment_sort_type,
|
||||
default_listing_type,
|
||||
theme: data.theme.clone(),
|
||||
interface_language: data.interface_language.clone(),
|
||||
|
@ -120,14 +142,21 @@ pub async fn save_user_settings(
|
|||
enable_keyboard_navigation: data.enable_keyboard_navigation,
|
||||
enable_animated_images: data.enable_animated_images,
|
||||
enable_private_messages: data.enable_private_messages,
|
||||
collapse_bot_comments: data.collapse_bot_comments,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Ignore errors, because 'no fields updated' will return an error.
|
||||
// https://github.com/LemmyNet/lemmy/issues/4076
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
|
||||
.await
|
||||
.ok();
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
|
||||
|
||||
// Update the vote display modes
|
||||
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {
|
||||
score: data.show_scores,
|
||||
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()))
|
||||
}
|
||||
|
|
|
@ -4,12 +4,9 @@ use lemmy_api_common::{
|
|||
context::LemmyContext,
|
||||
person::{UpdateTotp, UpdateTotpResponse},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::local_user::{LocalUser, LocalUserUpdateForm},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Enable or disable two-factor-authentication. The current setting is determined from
|
||||
/// [LocalUser.totp_2fa_enabled].
|
||||
|
@ -24,13 +21,11 @@ pub async fn update_totp(
|
|||
data: Json<UpdateTotp>,
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<UpdateTotpResponse>, LemmyError> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
|
||||
) -> LemmyResult<Json<UpdateTotpResponse>> {
|
||||
check_totp_2fa_valid(
|
||||
&local_user_view,
|
||||
&Some(data.totp_token.clone()),
|
||||
&site_view.site.name,
|
||||
&context.settings().hostname,
|
||||
)?;
|
||||
|
||||
// toggle the 2fa setting
|
||||
|
|
|
@ -4,7 +4,7 @@ use actix_web::{
|
|||
HttpRequest,
|
||||
};
|
||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
/// 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.
|
||||
|
@ -12,7 +12,7 @@ use lemmy_utils::error::{LemmyError, LemmyErrorType};
|
|||
pub async fn validate_auth(
|
||||
req: HttpRequest,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let jwt = read_auth_token(&req)?;
|
||||
if let Some(jwt) = jwt {
|
||||
local_user_view_from_jwt(&jwt, &context).await?;
|
||||
|
|
|
@ -5,17 +5,12 @@ use lemmy_api_common::{
|
|||
utils::send_new_applicant_email_to_admins,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
use lemmy_db_schema::source::{
|
||||
email_verification::EmailVerification,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
person::Person,
|
||||
},
|
||||
traits::Crud,
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
pub async fn verify_email(
|
||||
data: Json<VerifyEmail>,
|
||||
|
@ -23,9 +18,7 @@ pub async fn verify_email(
|
|||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let token = data.token.clone();
|
||||
let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::TokenNotFound)?;
|
||||
let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?;
|
||||
|
||||
let form = LocalUserUpdateForm {
|
||||
// necessary in case this is a new signup
|
||||
|
@ -36,16 +29,19 @@ pub async fn verify_email(
|
|||
};
|
||||
let local_user_id = verification.local_user_id;
|
||||
|
||||
let local_user = LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
||||
|
||||
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
||||
|
||||
// send out notification about registration application to admins if enabled
|
||||
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
&& site_view.local_site.application_email_admins
|
||||
{
|
||||
let person = Person::read(&mut context.pool(), local_user.person_id).await?;
|
||||
send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings())
|
||||
if site_view.local_site.application_email_admins {
|
||||
let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?;
|
||||
|
||||
send_new_applicant_email_to_admins(
|
||||
&local_user.person.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,14 +16,14 @@ use lemmy_db_schema::{
|
|||
PostFeatureType,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn feature_post(
|
||||
data: Json<FeaturePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_id = data.post_id;
|
||||
let orig_post = Post::read(&mut context.pool(), post_id).await?;
|
||||
|
||||
|
@ -70,11 +70,5 @@ pub async fn feature_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
||||
request::fetch_site_metadata,
|
||||
request::fetch_link_metadata,
|
||||
};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyResult},
|
||||
LemmyErrorType,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_link_metadata(
|
||||
data: Json<GetSiteMetadata>,
|
||||
data: Query<GetSiteMetadata>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<GetSiteMetadataResponse>, LemmyError> {
|
||||
let metadata = fetch_site_metadata(context.client(), &data.url).await?;
|
||||
// Require an account for this API
|
||||
_local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
||||
let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?;
|
||||
let metadata = fetch_link_metadata(&url, &context).await?;
|
||||
|
||||
Ok(Json(GetSiteMetadataResponse { metadata }))
|
||||
}
|
||||
|
|
34
crates/api/src/post/hide.rs
Normal file
34
crates/api/src/post/hide.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
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()))
|
||||
}
|
|
@ -5,7 +5,13 @@ use lemmy_api_common::{
|
|||
context::LemmyContext,
|
||||
post::{CreatePostLike, PostResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_user_action, check_downvotes_enabled, mark_post_as_read},
|
||||
utils::{
|
||||
check_bot_account,
|
||||
check_community_user_action,
|
||||
check_local_vote_mode,
|
||||
mark_post_as_read,
|
||||
VoteItem,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -16,7 +22,7 @@ use lemmy_db_schema::{
|
|||
traits::{Crud, Likeable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -24,14 +30,21 @@ pub async fn like_post(
|
|||
data: Json<CreatePostLike>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
let post_id = data.post_id;
|
||||
|
||||
// Don't do a downvote if site has downvotes disabled
|
||||
check_downvotes_enabled(data.score, &local_site)?;
|
||||
check_local_vote_mode(
|
||||
data.score,
|
||||
VoteItem::Post(post_id),
|
||||
&local_site,
|
||||
local_user_view.person.id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
check_bot_account(&local_user_view.person)?;
|
||||
|
||||
// Check for a community ban
|
||||
let post_id = data.post_id;
|
||||
let post = Post::read(&mut context.pool(), post_id).await?;
|
||||
|
||||
check_community_user_action(
|
||||
|
@ -60,25 +73,20 @@ pub async fn like_post(
|
|||
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
||||
}
|
||||
|
||||
// Mark the post as read
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
let community = Community::read(&mut context.pool(), post.community_id).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::LikePostOrComment(
|
||||
post.ap_id,
|
||||
local_user_view.person.clone(),
|
||||
Community::read(&mut context.pool(), post.community_id).await?,
|
||||
data.score,
|
||||
),
|
||||
SendActivityData::LikePostOrComment {
|
||||
object_id: post.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community,
|
||||
score: data.score,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
context.deref(),
|
||||
post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(context.deref(), post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
30
crates/api/src/post/list_post_likes.rs
Normal file
30
crates/api/src/post/list_post_likes.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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;
|
||||
|
||||
/// 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?;
|
||||
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 }))
|
||||
}
|
|
@ -15,14 +15,14 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn lock_post(
|
||||
data: Json<LockPost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_id = data.post_id;
|
||||
let orig_post = Post::read(&mut context.pool(), post_id).await?;
|
||||
|
||||
|
@ -61,11 +61,5 @@ pub async fn lock_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
|
|||
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
|
||||
use lemmy_db_schema::source::post::PostRead;
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, MAX_API_PARAM_ELEMENTS};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -10,15 +10,8 @@ pub async fn mark_post_as_read(
|
|||
data: Json<MarkPostAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
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);
|
||||
}
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
let post_ids = HashSet::from_iter(data.post_ids.clone());
|
||||
|
||||
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
|
||||
Err(LemmyErrorType::TooManyItems)?;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pub mod feature;
|
||||
pub mod get_link_metadata;
|
||||
pub mod hide;
|
||||
pub mod like;
|
||||
pub mod list_post_likes;
|
||||
pub mod lock;
|
||||
pub mod mark_read;
|
||||
pub mod save;
|
||||
|
|
|
@ -9,14 +9,14 @@ use lemmy_db_schema::{
|
|||
traits::Saveable,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_post(
|
||||
data: Json<SavePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let post_saved_form = PostSavedForm {
|
||||
post_id: data.post_id,
|
||||
person_id: local_user_view.person.id,
|
||||
|
@ -34,9 +34,14 @@ pub async fn save_post(
|
|||
|
||||
let post_id = data.post_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false).await?;
|
||||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mark the post as read
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
Ok(Json(PostResponse { post_view }))
|
||||
|
|
|
@ -5,7 +5,11 @@ use lemmy_api_common::{
|
|||
context::LemmyContext,
|
||||
post::{CreatePostReport, PostReportResponse},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{check_community_user_action, send_new_report_email_to_admins},
|
||||
utils::{
|
||||
check_community_user_action,
|
||||
check_post_deleted_or_removed,
|
||||
send_new_report_email_to_admins,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
|
@ -15,7 +19,7 @@ use lemmy_db_schema::{
|
|||
traits::Reportable,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostReportView, PostView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Creates a post report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -23,7 +27,7 @@ pub async fn create_post_report(
|
|||
data: Json<CreatePostReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
|
@ -40,6 +44,8 @@ pub async fn create_post_report(
|
|||
)
|
||||
.await?;
|
||||
|
||||
check_post_deleted_or_removed(&post_view.post)?;
|
||||
|
||||
let report_form = PostReportForm {
|
||||
creator_id: person_id,
|
||||
post_id,
|
||||
|
@ -67,12 +73,12 @@ pub async fn create_post_report(
|
|||
}
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::CreateReport(
|
||||
post_view.post.ap_id.inner().clone(),
|
||||
local_user_view.person,
|
||||
post_view.community,
|
||||
data.reason.clone(),
|
||||
),
|
||||
SendActivityData::CreateReport {
|
||||
object_id: post_view.post.ap_id.inner().clone(),
|
||||
actor: local_user_view.person,
|
||||
community: post_view.community,
|
||||
reason: data.reason.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -2,10 +2,10 @@ use actix_web::web::{Data, Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{ListPostReports, ListPostReportsResponse},
|
||||
utils::check_community_mod_action_opt,
|
||||
utils::check_community_mod_of_any_or_admin_action,
|
||||
};
|
||||
use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
/// Lists post reports for a community if an id is supplied
|
||||
/// or returns all post reports for communities a user moderates
|
||||
|
@ -14,16 +14,18 @@ pub async fn list_post_reports(
|
|||
data: Query<ListPostReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<ListPostReportsResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<ListPostReportsResponse>> {
|
||||
let community_id = data.community_id;
|
||||
let post_id = data.post_id;
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
|
||||
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
||||
|
||||
let page = data.page;
|
||||
let limit = data.limit;
|
||||
let post_reports = PostReportQuery {
|
||||
community_id,
|
||||
post_id,
|
||||
unresolved_only,
|
||||
page,
|
||||
limit,
|
||||
|
|
|
@ -6,7 +6,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{LocalUserView, PostReportView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
/// Resolves or unresolves a post report and notifies the moderators of the community
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -14,7 +14,7 @@ pub async fn resolve_post_report(
|
|||
data: Json<ResolvePostReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PostReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PostReportResponse>> {
|
||||
let report_id = data.report_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
|
||||
|
@ -23,7 +23,7 @@ pub async fn resolve_post_report(
|
|||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
report.community.id,
|
||||
false,
|
||||
true,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -8,14 +8,14 @@ use lemmy_db_schema::{
|
|||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn mark_pm_as_read(
|
||||
data: Json<MarkPrivateMessageAsRead>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PrivateMessageResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PrivateMessageResponse>> {
|
||||
// Checking permissions
|
||||
let private_message_id = data.private_message_id;
|
||||
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
|
||||
|
|
|
@ -14,14 +14,14 @@ use lemmy_db_schema::{
|
|||
traits::{Crud, Reportable},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn create_pm_report(
|
||||
data: Json<CreatePrivateMessageReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PrivateMessageReportResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let reason = data.reason.trim().to_string();
|
||||
|
@ -31,6 +31,11 @@ pub async fn create_pm_report(
|
|||
let private_message_id = data.private_message_id;
|
||||
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
|
||||
|
||||
// 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 {
|
||||
creator_id: person_id,
|
||||
private_message_id,
|
||||
|
|
|
@ -8,14 +8,14 @@ use lemmy_db_views::{
|
|||
private_message_report_view::PrivateMessageReportQuery,
|
||||
structs::LocalUserView,
|
||||
};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn list_pm_reports(
|
||||
data: Query<ListPrivateMessageReports>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<ListPrivateMessageReportsResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<ListPrivateMessageReportsResponse>> {
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
||||
|
|
|
@ -6,14 +6,14 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable};
|
||||
use lemmy_db_views::structs::{LocalUserView, PrivateMessageReportView};
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn resolve_pm_report(
|
||||
data: Json<ResolvePrivateMessageReport>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<PrivateMessageReportResponse>> {
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let report_id = data.report_id;
|
||||
|
|
|
@ -9,16 +9,20 @@ use lemmy_db_schema::{
|
|||
traits::Blockable,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn block_instance(
|
||||
data: Json<BlockInstance>,
|
||||
local_user_view: LocalUserView,
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<BlockInstanceResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<BlockInstanceResponse>> {
|
||||
let instance_id = data.instance_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 {
|
||||
person_id,
|
||||
instance_id,
|
||||
|
|
|
@ -5,12 +5,12 @@ use lemmy_api_common::{
|
|||
utils::build_federated_instances,
|
||||
};
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_federated_instances(
|
||||
context: Data<LemmyContext>,
|
||||
) -> Result<Json<GetFederatedInstancesResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
|
||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||
let federated_instances =
|
||||
build_federated_instances(&site_view.local_site, &mut context.pool()).await?;
|
||||
|
|
|
@ -4,24 +4,26 @@ use lemmy_db_schema::{
|
|||
source::{
|
||||
actor_language::SiteLanguage,
|
||||
language::Language,
|
||||
local_site_url_blocklist::LocalSiteUrlBlocklist,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
moderator::{ModAdd, ModAddForm},
|
||||
oauth_provider::OAuthProvider,
|
||||
tagline::Tagline,
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_db_views_actor::structs::PersonView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorType},
|
||||
version,
|
||||
error::{LemmyErrorType, LemmyResult},
|
||||
VERSION,
|
||||
};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn leave_admin(
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<GetSiteResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetSiteResponse>> {
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Make sure there isn't just one admin (so if one leaves, there will still be one left)
|
||||
|
@ -35,6 +37,9 @@ pub async fn leave_admin(
|
|||
local_user_view.local_user.id,
|
||||
&LocalUserUpdateForm {
|
||||
admin: Some(false),
|
||||
// Necessary because admins can bypass the registration applications (if they're turned on)
|
||||
// but then won't be able to log in because they haven't been approved.
|
||||
accepted_application: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
@ -56,18 +61,22 @@ pub async fn leave_admin(
|
|||
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
|
||||
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
|
||||
let custom_emojis =
|
||||
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
|
||||
let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?;
|
||||
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
|
||||
|
||||
Ok(Json(GetSiteResponse {
|
||||
site_view,
|
||||
admins,
|
||||
version: version::VERSION.to_string(),
|
||||
version: VERSION.to_string(),
|
||||
my_user: None,
|
||||
all_languages,
|
||||
discussion_languages,
|
||||
taglines,
|
||||
custom_emojis,
|
||||
oauth_providers: Some(oauth_providers),
|
||||
admin_oauth_providers: None,
|
||||
blocked_urls,
|
||||
tagline,
|
||||
taglines: vec![],
|
||||
custom_emojis: vec![],
|
||||
}))
|
||||
}
|
||||
|
|
23
crates/api/src/site/list_all_media.rs
Normal file
23
crates/api/src/site/list_all_media.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
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 }))
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod block;
|
||||
pub mod federated_instances;
|
||||
pub mod leave_admin;
|
||||
pub mod list_all_media;
|
||||
pub mod mod_log;
|
||||
pub mod purge;
|
||||
pub mod registration_applications;
|
||||
|
|
|
@ -2,7 +2,7 @@ use actix_web::web::{Data, Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{GetModlog, GetModlogResponse},
|
||||
utils::{check_community_mod_action_opt, check_private_instance, is_admin},
|
||||
utils::{check_community_mod_of_any_or_admin_action, check_private_instance},
|
||||
};
|
||||
use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
|
@ -24,7 +24,7 @@ use lemmy_db_views_moderator::structs::{
|
|||
ModTransferCommunityView,
|
||||
ModlogListParams,
|
||||
};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use ModlogActionType::*;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -32,7 +32,7 @@ pub async fn get_mod_log(
|
|||
data: Query<GetModlog>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
) -> Result<Json<GetModlogResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<GetModlogResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
@ -41,11 +41,9 @@ pub async fn get_mod_log(
|
|||
let community_id = data.community_id;
|
||||
|
||||
let is_mod_or_admin = if let Some(local_user_view) = local_user_view {
|
||||
let is_mod = community_id.is_some()
|
||||
&& check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool())
|
||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool())
|
||||
.await
|
||||
.is_ok();
|
||||
is_mod || is_admin(&local_user_view).is_ok()
|
||||
.is_ok()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
@ -57,10 +55,15 @@ pub async fn get_mod_log(
|
|||
data.mod_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 {
|
||||
community_id,
|
||||
mod_person_id,
|
||||
other_person_id,
|
||||
post_id,
|
||||
comment_id,
|
||||
page: data.page,
|
||||
limit: data.limit,
|
||||
hide_modlog_names,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
site::PurgeComment,
|
||||
utils::is_admin,
|
||||
SuccessResponse,
|
||||
|
@ -8,28 +10,42 @@ use lemmy_api_common::{
|
|||
use lemmy_db_schema::{
|
||||
source::{
|
||||
comment::Comment,
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn purge_comment(
|
||||
data: Json<PurgeComment>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Only let admin purge an item
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
|
||||
// Read the comment to get the post_id
|
||||
let comment = Comment::read(&mut context.pool(), comment_id).await?;
|
||||
// Read the comment to get the post_id and community
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let post_id = comment.post_id;
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![comment_view.creator.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let post_id = comment_view.comment.post_id;
|
||||
|
||||
// TODO read comments for pictrs images and purge them
|
||||
|
||||
|
@ -41,8 +57,18 @@ pub async fn purge_comment(
|
|||
reason: data.reason.clone(),
|
||||
post_id,
|
||||
};
|
||||
|
||||
AdminPurgeComment::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
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()))
|
||||
}
|
||||
|
|
|
@ -1,54 +1,82 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
request::purge_image_from_pictrs,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
site::PurgeCommunity,
|
||||
utils::{is_admin, purge_image_posts_for_community},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::PersonId,
|
||||
source::{
|
||||
community::Community,
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn purge_community(
|
||||
data: Json<PurgeCommunity>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Only let admin purge an item
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
|
||||
// Read the community to get its images
|
||||
let community = Community::read(&mut context.pool(), community_id).await?;
|
||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
||||
|
||||
if let Some(banner) = community.banner {
|
||||
purge_image_from_pictrs(&banner, &context).await.ok();
|
||||
// Also check that you're a higher admin than all the mods
|
||||
let community_mod_person_ids =
|
||||
CommunityModeratorView::for_community(&mut context.pool(), community.id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|cmv| cmv.moderator.id)
|
||||
.collect::<Vec<PersonId>>();
|
||||
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
community_mod_person_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(banner) = &community.banner {
|
||||
purge_image_from_pictrs(banner, &context).await.ok();
|
||||
}
|
||||
|
||||
if let Some(icon) = community.icon {
|
||||
purge_image_from_pictrs(&icon, &context).await.ok();
|
||||
if let Some(icon) = &community.icon {
|
||||
purge_image_from_pictrs(icon, &context).await.ok();
|
||||
}
|
||||
|
||||
purge_image_posts_for_community(community_id, &context).await?;
|
||||
purge_image_posts_for_community(data.community_id, &context).await?;
|
||||
|
||||
Community::delete(&mut context.pool(), community_id).await?;
|
||||
Community::delete(&mut context.pool(), data.community_id).await?;
|
||||
|
||||
// Mod tables
|
||||
let form = AdminPurgeCommunityForm {
|
||||
admin_person_id: local_user_view.person.id,
|
||||
reason: data.reason.clone(),
|
||||
};
|
||||
|
||||
AdminPurgeCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::RemoveCommunity {
|
||||
moderator: local_user_view.person.clone(),
|
||||
community,
|
||||
reason: data.reason.clone(),
|
||||
removed: true,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
||||
|
|
|
@ -1,53 +1,87 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use crate::ban_nonlocal_user_from_local_communities;
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
request::delete_image_from_pictrs,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
site::PurgePerson,
|
||||
utils::is_admin,
|
||||
utils::{is_admin, purge_user_account},
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
image_upload::ImageUpload,
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgePerson, AdminPurgePersonForm},
|
||||
person::Person,
|
||||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn purge_person(
|
||||
data: Json<PurgePerson>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Only let admin purge an item
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Read the person to get their images
|
||||
let person_id = data.person_id;
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let local_user = LocalUserView::read_person(&mut context.pool(), person_id).await?;
|
||||
let pictrs_uploads =
|
||||
ImageUpload::get_all_by_local_user_id(&mut context.pool(), &local_user.local_user.id).await?;
|
||||
let person = Person::read(&mut context.pool(), data.person_id).await?;
|
||||
|
||||
for upload in pictrs_uploads {
|
||||
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
ban_nonlocal_user_from_local_communities(
|
||||
&local_user_view,
|
||||
&person,
|
||||
true,
|
||||
&data.reason,
|
||||
&Some(true),
|
||||
&None,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Person::delete(&mut context.pool(), person_id).await?;
|
||||
// Clear profile data.
|
||||
purge_user_account(data.person_id, &context).await?;
|
||||
|
||||
// Keep person record, but mark as banned to prevent login or refetching from home instance.
|
||||
let person = Person::update(
|
||||
&mut context.pool(),
|
||||
data.person_id,
|
||||
&PersonUpdateForm {
|
||||
banned: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Mod tables
|
||||
let form = AdminPurgePersonForm {
|
||||
admin_person_id: local_user_view.person.id,
|
||||
reason: data.reason.clone(),
|
||||
};
|
||||
|
||||
AdminPurgePerson::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::BanFromSite {
|
||||
moderator: local_user_view.person,
|
||||
banned_user: person,
|
||||
reason: data.reason.clone(),
|
||||
remove_or_restore_data: Some(true),
|
||||
ban: true,
|
||||
expires: None,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
||||
|
|
|
@ -1,56 +1,73 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
request::purge_image_from_pictrs,
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
site::PurgePost,
|
||||
utils::is_admin,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgePost, AdminPurgePostForm},
|
||||
post::Post,
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn purge_post(
|
||||
data: Json<PurgePost>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<SuccessResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Only let admin purge an item
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let post_id = data.post_id;
|
||||
|
||||
// Read the post to get the community_id
|
||||
let post = Post::read(&mut context.pool(), post_id).await?;
|
||||
let post = Post::read(&mut context.pool(), data.post_id).await?;
|
||||
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![post.creator_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Purge image
|
||||
if let Some(url) = post.url {
|
||||
purge_image_from_pictrs(&url, &context).await.ok();
|
||||
if let Some(url) = &post.url {
|
||||
purge_image_from_pictrs(url, &context).await.ok();
|
||||
}
|
||||
// Purge thumbnail
|
||||
if let Some(thumbnail_url) = post.thumbnail_url {
|
||||
purge_image_from_pictrs(&thumbnail_url, &context).await.ok();
|
||||
if let Some(thumbnail_url) = &post.thumbnail_url {
|
||||
purge_image_from_pictrs(thumbnail_url, &context).await.ok();
|
||||
}
|
||||
|
||||
let community_id = post.community_id;
|
||||
|
||||
Post::delete(&mut context.pool(), post_id).await?;
|
||||
Post::delete(&mut context.pool(), data.post_id).await?;
|
||||
|
||||
// Mod tables
|
||||
let form = AdminPurgePostForm {
|
||||
admin_person_id: local_user_view.person.id,
|
||||
reason: data.reason.clone(),
|
||||
community_id,
|
||||
community_id: post.community_id,
|
||||
};
|
||||
|
||||
AdminPurgePost::create(&mut context.pool(), &form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::RemovePost {
|
||||
post,
|
||||
moderator: local_user_view.person.clone(),
|
||||
reason: data.reason.clone(),
|
||||
removed: true,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(SuccessResponse::default()))
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use actix_web::web::{Data, Json};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{ApproveRegistrationApplication, RegistrationApplicationResponse},
|
||||
|
@ -10,48 +11,60 @@ use lemmy_db_schema::{
|
|||
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::diesel_option_overwrite,
|
||||
utils::{diesel_string_update, get_conn},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
||||
use lemmy_utils::error::LemmyError;
|
||||
use lemmy_utils::error::{LemmyError, LemmyResult};
|
||||
|
||||
pub async fn approve_registration_application(
|
||||
data: Json<ApproveRegistrationApplication>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> Result<Json<RegistrationApplicationResponse>, LemmyError> {
|
||||
) -> LemmyResult<Json<RegistrationApplicationResponse>> {
|
||||
let app_id = data.id;
|
||||
|
||||
// Only let admins do this
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
let pool = &mut context.pool();
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let tx_data = data.clone();
|
||||
let approved_user_id = conn
|
||||
.build_transaction()
|
||||
.run(|conn| {
|
||||
Box::pin(async move {
|
||||
// Update the registration with reason, admin_id
|
||||
let deny_reason = diesel_option_overwrite(data.deny_reason.clone());
|
||||
let deny_reason = diesel_string_update(tx_data.deny_reason.as_deref());
|
||||
let app_form = RegistrationApplicationUpdateForm {
|
||||
admin_id: Some(Some(local_user_view.person.id)),
|
||||
deny_reason,
|
||||
};
|
||||
|
||||
let registration_application =
|
||||
RegistrationApplication::update(&mut context.pool(), app_id, &app_form).await?;
|
||||
RegistrationApplication::update(&mut conn.into(), app_id, &app_form).await?;
|
||||
|
||||
// Update the local_user row
|
||||
let local_user_form = LocalUserUpdateForm {
|
||||
accepted_application: Some(data.approve),
|
||||
accepted_application: Some(tx_data.approve),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let approved_user_id = registration_application.local_user_id;
|
||||
LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?;
|
||||
LocalUser::update(&mut conn.into(), approved_user_id, &local_user_form).await?;
|
||||
|
||||
Ok::<_, LemmyError>(approved_user_id)
|
||||
}) as _
|
||||
})
|
||||
.await?;
|
||||
|
||||
if data.approve {
|
||||
let approved_local_user_view =
|
||||
LocalUserView::read(&mut context.pool(), approved_user_id).await?;
|
||||
|
||||
if approved_local_user_view.local_user.email.is_some() {
|
||||
// Email sending may fail, but this won't revert the application approval
|
||||
send_application_approved_email(&approved_local_user_view, context.settings()).await?;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Read the view
|
||||
let registration_application =
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue