mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-01-15 14:35:54 +00:00
Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9b1e7c4400 | ||
|
a529912296 | ||
|
0d31182ef0 | ||
|
15d754250f | ||
|
30bcd2b820 | ||
|
7386e48c4a | ||
|
869714e2f3 |
1453 changed files with 50750 additions and 119101 deletions
14
.dockerignore
vendored
14
.dockerignore
vendored
|
@ -1,8 +1,6 @@
|
||||||
# build folders and similar which are not needed for the docker build
|
ui/node_modules
|
||||||
target
|
ui/dist
|
||||||
docker
|
server/target
|
||||||
api_tests
|
docker/dev/volumes
|
||||||
ansible
|
docker/federation/volumes
|
||||||
tests
|
.git
|
||||||
*.sh
|
|
||||||
pictrs
|
|
||||||
|
|
4
.gitattributes
vendored
4
.gitattributes
vendored
|
@ -1,2 +1,2 @@
|
||||||
# Normalize EOL for all files that Git considers text files.
|
* linguist-vendored
|
||||||
* text=auto eol=lf
|
*.rs linguist-vendored=false
|
||||||
|
|
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -1,3 +0,0 @@
|
||||||
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917
|
|
||||||
crates/apub/ @Nutomic
|
|
||||||
migrations/ @dessalines @phiresky @dullbananas
|
|
70
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
70
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
|
@ -1,70 +0,0 @@
|
||||||
name: "\U0001F41E Bug Report"
|
|
||||||
description: Create a report to help us improve lemmy
|
|
||||||
title: "[Bug]: "
|
|
||||||
labels: ["bug", "triage"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Found a bug? Please fill out the sections below. 👍
|
|
||||||
Thanks for taking the time to fill out this bug report!
|
|
||||||
For front end issues, use [lemmy](https://github.com/LemmyNet/lemmy-ui)
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Requirements
|
|
||||||
description: Before you create a bug report please do the following.
|
|
||||||
options:
|
|
||||||
- label: Is this a bug report? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
|
||||||
required: true
|
|
||||||
- label: Did you check to see if this issue already exists?
|
|
||||||
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
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: Summary
|
|
||||||
description: A summary of the bug.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: reproduce
|
|
||||||
attributes:
|
|
||||||
label: Steps to Reproduce
|
|
||||||
description: |
|
|
||||||
Describe the steps to reproduce the bug.
|
|
||||||
The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ resolution.
|
|
||||||
value: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: technical
|
|
||||||
attributes:
|
|
||||||
label: Technical Details
|
|
||||||
description: |
|
|
||||||
- Please post your log: `sudo docker-compose logs > lemmy_log.out`.
|
|
||||||
- What OS are you trying to install lemmy on?
|
|
||||||
- Any browser console errors?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: lemmy-backend-version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: Which Lemmy backend version do you use? Displayed in the footer.
|
|
||||||
placeholder: ex. BE 0.17.4
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: lemmy-instance
|
|
||||||
attributes:
|
|
||||||
label: Lemmy Instance URL
|
|
||||||
description: Which Lemmy instance do you use? The address
|
|
||||||
placeholder: lemmy.ml, lemmy.world, etc
|
|
56
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
56
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
|
@ -1,56 +0,0 @@
|
||||||
name: "\U0001F680 Feature request"
|
|
||||||
description: Suggest an idea for improving Lemmy
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Have a suggestion about Lemmy's UI?
|
|
||||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Requirements
|
|
||||||
description: Before you create a bug report please do the following.
|
|
||||||
options:
|
|
||||||
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
|
||||||
required: true
|
|
||||||
- label: Did you check to see if this issue already exists?
|
|
||||||
required: true
|
|
||||||
- label: Is this only a feature request? Do not put multiple feature requests in one issue.
|
|
||||||
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:
|
|
||||||
label: Is your proposal related to a problem?
|
|
||||||
description: |
|
|
||||||
Provide a clear and concise description of what the problem is.
|
|
||||||
For example, "I'm always frustrated when..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: Describe the solution you'd like.
|
|
||||||
description: |
|
|
||||||
Provide a clear and concise description of what you want to happen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: Describe alternatives you've considered.
|
|
||||||
description: |
|
|
||||||
Let us know about other solutions you've tried or researched.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: |
|
|
||||||
Is there anything else you can add about the proposal?
|
|
||||||
You might want to link to related issues here, if you haven't already.
|
|
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
17
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
|
@ -1,17 +0,0 @@
|
||||||
name: "? Question"
|
|
||||||
description: General questions about Lemmy
|
|
||||||
title: "Question: "
|
|
||||||
labels: ["question", "triage"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Have a question about Lemmy?
|
|
||||||
Please check the docs first: https://join-lemmy.org/docs/en/index.html
|
|
||||||
- type: textarea
|
|
||||||
id: question
|
|
||||||
attributes:
|
|
||||||
label: Question
|
|
||||||
description: What's the question you have about Lemmy?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
38
.gitignore
vendored
38
.gitignore
vendored
|
@ -1,36 +1,10 @@
|
||||||
# local ansible configuration
|
|
||||||
ansible/inventory
|
ansible/inventory
|
||||||
|
ansible/inventory_dev
|
||||||
ansible/passwords/
|
ansible/passwords/
|
||||||
|
|
||||||
# docker build files
|
|
||||||
docker/lemmy_mine.hjson
|
docker/lemmy_mine.hjson
|
||||||
docker/dev/env_deploy.sh
|
docker/dev/env_deploy.sh
|
||||||
volumes
|
build/
|
||||||
|
.idea/
|
||||||
# ide config
|
ui/src/translations
|
||||||
.idea
|
docker/dev/volumes
|
||||||
.vscode
|
docker/federation-test/volumes
|
||||||
|
|
||||||
# local build files
|
|
||||||
target
|
|
||||||
env_setup.sh
|
|
||||||
query_testing/**/reports/*.json
|
|
||||||
|
|
||||||
# API tests
|
|
||||||
api_tests/node_modules
|
|
||||||
api_tests/.yalc
|
|
||||||
api_tests/yalc.lock
|
|
||||||
api_tests/pict-rs
|
|
||||||
|
|
||||||
# pictrs data
|
|
||||||
pictrs/
|
|
||||||
|
|
||||||
# The generated typescript bindings
|
|
||||||
bindings
|
|
||||||
|
|
||||||
# Database cluster and sockets for testing
|
|
||||||
dev_pgdata/
|
|
||||||
*.PGSQL.*
|
|
||||||
|
|
||||||
# database dumps
|
|
||||||
*.sqldump
|
|
||||||
|
|
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
||||||
[submodule "crates/utils/translations"]
|
|
||||||
path = crates/utils/translations
|
|
||||||
url = https://github.com/LemmyNet/lemmy-translations.git
|
|
||||||
branch = main
|
|
7
.rustfmt.toml
vendored
7
.rustfmt.toml
vendored
|
@ -1,7 +0,0 @@
|
||||||
tab_spaces = 2
|
|
||||||
edition = "2021"
|
|
||||||
imports_layout = "HorizontalVertical"
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
group_imports = "One"
|
|
||||||
wrap_comments = true
|
|
||||||
comment_width = 100
|
|
34
.travis.yml
vendored
Normal file
34
.travis.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
language: rust
|
||||||
|
rust:
|
||||||
|
- stable
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- rust: nightly
|
||||||
|
fast_finish: true
|
||||||
|
cache: cargo
|
||||||
|
before_cache:
|
||||||
|
- rm -rfv target/debug/incremental/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/.fingerprint/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/build/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/deps/lemmy_server-*
|
||||||
|
- rm -rfv target/debug/lemmy_server.d
|
||||||
|
before_script:
|
||||||
|
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
|
- psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
|
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
|
||||||
|
before_install:
|
||||||
|
- cd server
|
||||||
|
script:
|
||||||
|
# Default checks, but fail if anything is detected
|
||||||
|
- cargo build
|
||||||
|
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
|
||||||
|
- cargo install diesel_cli --no-default-features --features postgres --force
|
||||||
|
- diesel migration run
|
||||||
|
- cargo test
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
|
- RUST_TEST_THREADS=1
|
||||||
|
|
||||||
|
addons:
|
||||||
|
postgresql: "9.4"
|
311
.woodpecker.yml
vendored
311
.woodpecker.yml
vendored
|
@ -1,311 +0,0 @@
|
||||||
# TODO: The when: platform conditionals aren't working currently
|
|
||||||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
|
||||||
|
|
||||||
variables:
|
|
||||||
# When updating the rust version here, be sure to update versions in `docker/Dockerfile`
|
|
||||||
# as well. Otherwise release builds can fail if Lemmy or dependencies rely on new Rust
|
|
||||||
# features. In particular the ARM builder image needs to be updated manually in the repo below:
|
|
||||||
# https://github.com/raskyld/lemmy-cross-toolchains
|
|
||||||
- &rust_image "rust:1.83"
|
|
||||||
- &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
|
|
||||||
- event: pull_request
|
|
||||||
path:
|
|
||||||
include: [
|
|
||||||
# rust source code
|
|
||||||
"crates/**",
|
|
||||||
"src/**",
|
|
||||||
"**/Cargo.toml",
|
|
||||||
"Cargo.lock",
|
|
||||||
# database migrations
|
|
||||||
"migrations/**",
|
|
||||||
# typescript tests
|
|
||||||
"api_tests/**",
|
|
||||||
# config files and scripts used by ci
|
|
||||||
".woodpecker.yml",
|
|
||||||
".rustfmt.toml",
|
|
||||||
"scripts/update_config_defaults.sh",
|
|
||||||
"diesel.toml",
|
|
||||||
".gitmodules",
|
|
||||||
]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
prepare_repo:
|
|
||||||
image: alpine:3
|
|
||||||
commands:
|
|
||||||
- apk add git
|
|
||||||
- git submodule init
|
|
||||||
- git submodule update
|
|
||||||
when:
|
|
||||||
- event: [pull_request, tag]
|
|
||||||
|
|
||||||
prettier_check:
|
|
||||||
image: tmknom/prettier:3.2.5
|
|
||||||
commands:
|
|
||||||
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
toml_fmt:
|
|
||||||
image: tamasfe/taplo:0.9.3
|
|
||||||
commands:
|
|
||||||
- taplo format --check
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
sql_fmt:
|
|
||||||
image: backplane/pgformatter
|
|
||||||
commands:
|
|
||||||
- ./scripts/sql_format_check.sh
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
cargo_fmt:
|
|
||||||
image: *rust_nightly_image
|
|
||||||
environment:
|
|
||||||
# store cargo data in repo folder so that it gets cached between steps
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- rustup component add rustfmt
|
|
||||||
- cargo +nightly fmt -- --check
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
cargo_clippy:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- rustup component add clippy
|
|
||||||
- cargo clippy --workspace --tests --all-targets -- -D warnings
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
# `DROP OWNED` doesn't work for default user
|
|
||||||
create_database_user:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
environment:
|
|
||||||
PGUSER: postgres
|
|
||||||
PGPASSWORD: password
|
|
||||||
PGHOST: database
|
|
||||||
PGDATABASE: lemmy
|
|
||||||
commands:
|
|
||||||
- psql -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;"
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
cargo_test:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
LEMMY_TEST_FAST_FEDERATION: "1"
|
|
||||||
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
|
|
||||||
commands:
|
|
||||||
# Install pg_dump for the schema setup test (must match server version)
|
|
||||||
- apt update && apt install -y lsb-release
|
|
||||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
|
||||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
|
||||||
- apt update && apt install -y postgresql-client-16
|
|
||||||
# Run tests
|
|
||||||
- cargo test --workspace --no-fail-fast
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_ts_bindings:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- ./scripts/ts_bindings_check.sh
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
# make sure api builds with default features (used by other crates relying on lemmy api)
|
|
||||||
check_api_common_default_features:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- cargo check --package lemmy_api_common
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
lemmy_api_common_doesnt_depend_on_diesel:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
lemmy_api_common_works_with_wasm:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- "rustup target add wasm32-unknown-unknown"
|
|
||||||
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_defaults_hjson_updated:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
|
|
||||||
- diff config/defaults.hjson config/defaults_current.hjson
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
cargo_build:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- cargo build
|
|
||||||
- mv target/debug/lemmy_server target/lemmy_server
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_diesel_schema:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- cp crates/db_schema/src/schema.rs tmp.schema
|
|
||||||
- target/lemmy_server migration --all run
|
|
||||||
- <<: *install_diesel_cli
|
|
||||||
- diesel print-schema
|
|
||||||
- diff tmp.schema crates/db_schema/src/schema.rs
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_db_perf_tool:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
# same as scripts/db_perf.sh but without creating a new database server
|
|
||||||
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
run_federation_tests:
|
|
||||||
image: node:22-bookworm-slim
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
|
|
||||||
DO_WRITE_HOSTS_FILE: "1"
|
|
||||||
commands:
|
|
||||||
- *install_pnpm
|
|
||||||
- apt-get update && apt-get install -y bash curl postgresql-client
|
|
||||||
- bash api_tests/prepare-drone-federation-test.sh
|
|
||||||
- cd api_tests/
|
|
||||||
- pnpm i
|
|
||||||
- pnpm api-test
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
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: pull_request
|
|
||||||
status: failure
|
|
||||||
|
|
||||||
publish_release_docker:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
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: ${CI_COMMIT_TAG}
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
nightly_build:
|
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
|
||||||
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
|
|
||||||
|
|
||||||
# using https://github.com/pksunkara/cargo-workspaces
|
|
||||||
publish_to_crates_io:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_API_TOKEN:
|
|
||||||
from_secret: cargo_api_token
|
|
||||||
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}"
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
notify_on_build:
|
|
||||||
image: alpine:3
|
|
||||||
commands:
|
|
||||||
- apk add curl
|
|
||||||
- "curl -d'Lemmy CI build ${CI_PIPELINE_STATUS}: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
|
|
||||||
when:
|
|
||||||
- event: [pull_request, tag]
|
|
||||||
status: [failure, success]
|
|
||||||
|
|
||||||
notify_on_tag_deploy:
|
|
||||||
image: alpine:3
|
|
||||||
commands:
|
|
||||||
- apk add curl
|
|
||||||
- "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
services:
|
|
||||||
database:
|
|
||||||
# 15-alpine image necessary because of diesel tests
|
|
||||||
image: pgautoupgrade/pgautoupgrade:15-alpine
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: lemmy
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: password
|
|
35
CODE_OF_CONDUCT.md
vendored
Normal file
35
CODE_OF_CONDUCT.md
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
- We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
|
||||||
|
- Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
|
||||||
|
- Please be kind and courteous. There’s no need to be mean or rude.
|
||||||
|
- Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
|
||||||
|
- Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
|
||||||
|
- We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the Citizen Code of Conduct; if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don’t tolerate behavior that excludes people in socially marginalized groups.
|
||||||
|
- Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or any of the Lemmy moderation team immediately. Whether you’re a regular contributor or a newcomer, we care about making this community a safe place for you and we’ve got your back.
|
||||||
|
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
|
||||||
|
|
||||||
|
[**Message the Moderation Team on Mastodon**](https://mastodon.social/@LemmyDev)
|
||||||
|
|
||||||
|
[**Email The Moderation Team**](mailto:contact@lemmy.ml)
|
||||||
|
|
||||||
|
## Moderation
|
||||||
|
|
||||||
|
These are the policies for upholding our community’s standards of conduct. If you feel that a thread needs moderation, please contact the Lemmy moderation team .
|
||||||
|
|
||||||
|
1. Remarks that violate the Lemmy standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.)
|
||||||
|
2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed.
|
||||||
|
3. Moderators will first respond to such remarks with a warning, at the same time the offending content will likely be removed whenever possible.
|
||||||
|
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
|
||||||
|
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
|
||||||
|
6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
|
||||||
|
7. If a moderator bans someone and you think it was unjustified, please take it up with that moderator, or with a different moderator, in private. Complaints about bans in-channel are not allowed.
|
||||||
|
8. Moderators are held to a higher standard than other community members. If a moderator creates an inappropriate situation, they should expect less leeway than others.
|
||||||
|
|
||||||
|
In the Lemmy community we strive to go the extra step to look out for each other. Don’t just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if they’re off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely.
|
||||||
|
|
||||||
|
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
|
||||||
|
|
||||||
|
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/LemmyNet/lemmy](https://yerbamate.dev/LemmyNet/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
|
||||||
|
|
||||||
|
Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).
|
4
CONTRIBUTING.md
vendored
Normal file
4
CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
See [here](https://dev.lemmy.ml/docs/contributing.html) for contributing Instructions.
|
||||||
|
|
6230
Cargo.lock
generated
vendored
6230
Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
199
Cargo.toml
vendored
199
Cargo.toml
vendored
|
@ -1,199 +0,0 @@
|
||||||
[workspace.package]
|
|
||||||
version = "0.19.6-beta.7"
|
|
||||||
edition = "2021"
|
|
||||||
description = "A link aggregator for the fediverse"
|
|
||||||
license = "AGPL-3.0"
|
|
||||||
homepage = "https://join-lemmy.org/"
|
|
||||||
documentation = "https://join-lemmy.org/docs/en/index.html"
|
|
||||||
repository = "https://github.com/LemmyNet/lemmy"
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "lemmy_server"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
description.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
documentation.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
# See https://github.com/johnthagen/min-sized-rust for additional optimizations
|
|
||||||
[profile.release]
|
|
||||||
lto = "fat"
|
|
||||||
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.
|
|
||||||
[profile.dev]
|
|
||||||
debug = 0
|
|
||||||
|
|
||||||
[features]
|
|
||||||
json-log = ["tracing-subscriber/json"]
|
|
||||||
default = []
|
|
||||||
|
|
||||||
[workspace]
|
|
||||||
members = [
|
|
||||||
"crates/api",
|
|
||||||
"crates/api_crud",
|
|
||||||
"crates/api_common",
|
|
||||||
"crates/apub",
|
|
||||||
"crates/utils",
|
|
||||||
"crates/db_perf",
|
|
||||||
"crates/db_schema",
|
|
||||||
"crates/db_views",
|
|
||||||
"crates/db_views_actor",
|
|
||||||
"crates/db_views_actor",
|
|
||||||
"crates/routes",
|
|
||||||
"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"
|
|
||||||
unused_async = "deny"
|
|
||||||
map_err_ignore = "deny"
|
|
||||||
expect_used = "deny"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
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.1", default-features = false, features = [
|
|
||||||
"actix-web",
|
|
||||||
] }
|
|
||||||
diesel = "2.2.6"
|
|
||||||
diesel_migrations = "2.2.0"
|
|
||||||
diesel-async = "0.5.2"
|
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
|
||||||
serde_with = "3.12.0"
|
|
||||||
actix-web = { version = "4.9.0", default-features = false, features = [
|
|
||||||
"compress-brotli",
|
|
||||||
"compress-gzip",
|
|
||||||
"compress-zstd",
|
|
||||||
"cookies",
|
|
||||||
"macros",
|
|
||||||
"rustls-0_23",
|
|
||||||
] }
|
|
||||||
tracing = "0.1.41"
|
|
||||||
tracing-actix-web = { version = "0.7.15", default-features = false }
|
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
|
||||||
url = { version = "2.5.4", features = ["serde"] }
|
|
||||||
reqwest = { version = "0.12.12", default-features = false, features = [
|
|
||||||
"blocking",
|
|
||||||
"gzip",
|
|
||||||
"json",
|
|
||||||
"rustls-tls",
|
|
||||||
] }
|
|
||||||
reqwest-middleware = "0.3.3"
|
|
||||||
reqwest-tracing = "0.5.5"
|
|
||||||
clokwerk = "0.4.0"
|
|
||||||
doku = { version = "0.21.1", features = ["url-2"] }
|
|
||||||
bcrypt = "0.16.0"
|
|
||||||
chrono = { version = "0.4.39", features = [
|
|
||||||
"now",
|
|
||||||
"serde",
|
|
||||||
], default-features = false }
|
|
||||||
serde_json = { version = "1.0.135", features = ["preserve_order"] }
|
|
||||||
base64 = "0.22.1"
|
|
||||||
uuid = { version = "1.12.0", features = ["serde"] }
|
|
||||||
async-trait = "0.1.85"
|
|
||||||
captcha = "0.0.9"
|
|
||||||
anyhow = { version = "1.0.95", features = ["backtrace"] }
|
|
||||||
diesel_ltree = "0.4.0"
|
|
||||||
serial_test = "3.2.0"
|
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
|
||||||
regex = "1.11.1"
|
|
||||||
diesel-derive-newtype = "2.1.2"
|
|
||||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
|
||||||
strum = { version = "0.26.3", features = ["derive"] }
|
|
||||||
itertools = "0.14.0"
|
|
||||||
futures = "0.3.31"
|
|
||||||
http = "1.2"
|
|
||||||
rosetta-i18n = "0.1.3"
|
|
||||||
ts-rs = { version = "10.1.0", features = [
|
|
||||||
"chrono-impl",
|
|
||||||
"no-serde-warnings",
|
|
||||||
"url-impl",
|
|
||||||
] }
|
|
||||||
rustls = { version = "0.23.21", features = ["ring"] }
|
|
||||||
futures-util = "0.3.31"
|
|
||||||
tokio-postgres = "0.7.12"
|
|
||||||
tokio-postgres-rustls = "0.13.0"
|
|
||||||
urlencoding = "2.1.3"
|
|
||||||
enum-map = "2.7"
|
|
||||||
moka = { version = "0.12.10", features = ["future"] }
|
|
||||||
i-love-jesus = { version = "0.1.0" }
|
|
||||||
clap = { version = "4.5.26", features = ["derive", "env"] }
|
|
||||||
pretty_assertions = "1.4.1"
|
|
||||||
derive-new = "0.7.0"
|
|
||||||
diesel-bind-if-some = "0.1.0"
|
|
||||||
tuplex = "0.1.2"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
lemmy_api = { workspace = true }
|
|
||||||
lemmy_api_crud = { workspace = true }
|
|
||||||
lemmy_apub = { workspace = true }
|
|
||||||
lemmy_utils = { workspace = true }
|
|
||||||
lemmy_db_schema = { workspace = true }
|
|
||||||
lemmy_api_common = { workspace = true }
|
|
||||||
lemmy_routes = { workspace = true }
|
|
||||||
lemmy_federate = { workspace = true }
|
|
||||||
activitypub_federation = { workspace = true }
|
|
||||||
diesel = { workspace = true }
|
|
||||||
diesel-async = { workspace = true }
|
|
||||||
actix-web = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
tracing-actix-web = { workspace = true }
|
|
||||||
tracing-subscriber = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
reqwest-middleware = { workspace = true }
|
|
||||||
reqwest-tracing = { workspace = true }
|
|
||||||
clokwerk = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
rustls = { workspace = true }
|
|
||||||
tokio.workspace = true
|
|
||||||
actix-cors = "0.7.0"
|
|
||||||
futures-util = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
|
||||||
prometheus = { version = "0.13.4", features = ["process"] }
|
|
||||||
serial_test = { workspace = true }
|
|
||||||
clap = { workspace = true }
|
|
||||||
actix-web-prom = "0.9.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_assertions = { workspace = true }
|
|
94
README.md
vendored
94
README.md
vendored
|
@ -1,67 +1,54 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
|
||||||
[![Build Status](https://woodpecker.join-lemmy.org/api/badges/LemmyNet/lemmy/status.svg)](https://woodpecker.join-lemmy.org/LemmyNet/lemmy)
|
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy)
|
||||||
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
|
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
|
||||||
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
|
||||||
[![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/)
|
[![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
|
||||||
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
|
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
|
||||||
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
|
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
|
||||||
<a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<span>English</span> |
|
<a href="https://dev.lemmy.ml/" rel="noopener">
|
||||||
<a href="readmes/README.es.md">Español</a> |
|
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
|
||||||
<a href="readmes/README.ru.md">Русский</a> |
|
|
||||||
<a href="readmes/README.zh.hans.md">汉语</a> |
|
|
||||||
<a href="readmes/README.zh.hant.md">漢語</a> |
|
|
||||||
<a href="readmes/README.ja.md">日本語</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<h3 align="center"><a href="https://dev.lemmy.ml">Lemmy</a></h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://join-lemmy.org/" rel="noopener">
|
A link aggregator / reddit clone for the fediverse.
|
||||||
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmyNet/lemmy-ui/main/src/assets/icons/favicon.svg"></a>
|
|
||||||
|
|
||||||
<h3 align="center"><a href="https://join-lemmy.org">Lemmy</a></h3>
|
|
||||||
<p align="center">
|
|
||||||
A link aggregator and forum for the fediverse.
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a href="https://join-lemmy.org">Join Lemmy</a>
|
<a href="https://dev.lemmy.ml">View Site</a>
|
||||||
·
|
·
|
||||||
<a href="https://join-lemmy.org/docs/index.html">Documentation</a>
|
<a href="https://dev.lemmy.ml/docs/index.html">Documentation</a>
|
||||||
·
|
|
||||||
<a href="https://matrix.to/#/#lemmy-space:matrix.org">Matrix Chat</a>
|
|
||||||
·
|
·
|
||||||
<a href="https://github.com/LemmyNet/lemmy/issues">Report Bug</a>
|
<a href="https://github.com/LemmyNet/lemmy/issues">Report Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
|
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/LemmyNet/lemmy/blob/main/RELEASES.md">Releases</a>
|
<a href="https://github.com/LemmyNet/lemmy/blob/master/RELEASES.md">Releases</a>
|
||||||
·
|
|
||||||
<a href="https://join-lemmy.org/docs/code_of_conduct.html">Code of Conduct</a>
|
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## About The Project
|
## About The Project
|
||||||
|
|
||||||
| Desktop | Mobile |
|
Front Page|Post
|
||||||
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
---|---
|
||||||
| ![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) |
|
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
|
||||||
|
|
||||||
[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).
|
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), 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).
|
||||||
|
|
||||||
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
|
||||||
|
|
||||||
It is an easily self-hostable, decentralized alternative to Reddit and other link aggregators, outside of their corporate control and meddling.
|
The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
|
||||||
|
|
||||||
Each Lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
|
||||||
|
|
||||||
|
*Note: Federation is still in active development and the WebSocket, as well as, HTTP API are currently unstable*
|
||||||
|
|
||||||
### Why's it called Lemmy?
|
### Why's it called Lemmy?
|
||||||
|
|
||||||
- Lead singer from [Motörhead](https://invidio.us/watch?v=3mbvWn1EY6g).
|
- Lead singer from [Motörhead](https://invidio.us/watch?v=pWB5JZRGl0U).
|
||||||
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
- The old school [video game](<https://en.wikipedia.org/wiki/Lemmings_(video_game)>).
|
||||||
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
- The [Koopa from Super Mario](https://www.mariowiki.com/Lemmy_Koopa).
|
||||||
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
- The [furry rodents](http://sunchild.fpwc.org/lemming-the-little-giant-of-the-north/).
|
||||||
|
@ -78,15 +65,15 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
|
|
||||||
- Open source, [AGPL License](/LICENSE).
|
- Open source, [AGPL License](/LICENSE).
|
||||||
- Self hostable, easy to deploy.
|
- Self hostable, easy to deploy.
|
||||||
- Comes with [Docker](https://join-lemmy.org/docs/administration/install_docker.html) and [Ansible](https://join-lemmy.org/docs/administration/install_ansible.html).
|
- Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
|
||||||
- Clean, mobile-friendly interface.
|
- Clean, mobile-friendly interface.
|
||||||
- Only a minimum of a username and password is required to sign up!
|
- Only a minimum of a username and password is required to sign up!
|
||||||
- User avatar support.
|
- User avatar support.
|
||||||
- Live-updating Comment threads.
|
- Live-updating Comment threads.
|
||||||
- Full vote scores `(+/-)` like old Reddit.
|
- Full vote scores `(+/-)` like old reddit.
|
||||||
- Themes, including light, dark, and solarized.
|
- Themes, including light, dark, and solarized.
|
||||||
- Emojis with autocomplete support. Start typing `:`
|
- Emojis with autocomplete support. Start typing `:`
|
||||||
- User tagging using `@`, Community tagging using `!`.
|
- User tagging using `@`, Community tagging using `#`.
|
||||||
- Integrated image uploading in both posts and comments.
|
- Integrated image uploading in both posts and comments.
|
||||||
- A post can consist of a title and any combination of self text, a URL, or nothing else.
|
- A post can consist of a title and any combination of self text, a URL, or nothing else.
|
||||||
- Notifications, on comment replies and when you're tagged.
|
- Notifications, on comment replies and when you're tagged.
|
||||||
|
@ -95,7 +82,7 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
- i18n / internationalization support.
|
- i18n / internationalization support.
|
||||||
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
- RSS / Atom feeds for `All`, `Subscribed`, `Inbox`, `User`, and `Community`.
|
||||||
- Cross-posting support.
|
- Cross-posting support.
|
||||||
- A _similar post search_ when creating new posts. Great for question / answer communities.
|
- A *similar post search* when creating new posts. Great for question / answer communities.
|
||||||
- Moderation abilities.
|
- Moderation abilities.
|
||||||
- Public Moderation Logs.
|
- Public Moderation Logs.
|
||||||
- Can sticky posts to the top of communities.
|
- Can sticky posts to the top of communities.
|
||||||
|
@ -105,28 +92,26 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
- Can transfer site and communities to others.
|
- Can transfer site and communities to others.
|
||||||
- Can fully erase your data, replacing all posts and comments.
|
- Can fully erase your data, replacing all posts and comments.
|
||||||
- NSFW post / community support.
|
- NSFW post / community support.
|
||||||
|
- OEmbed support via Iframely.
|
||||||
- High performance.
|
- High performance.
|
||||||
- Server is written in rust.
|
- Server is written in rust.
|
||||||
|
- Front end is `~80kB` gzipped.
|
||||||
- Supports arm64 / Raspberry Pi.
|
- Supports arm64 / Raspberry Pi.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
- [Lemmy Administration Docs](https://join-lemmy.org/docs/administration/administration.html)
|
- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
|
||||||
|
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
|
||||||
## Lemmy Projects
|
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
|
||||||
|
|
||||||
- [awesome-lemmy - A community driven list of apps and tools for lemmy](https://github.com/dbeley/awesome-lemmy)
|
|
||||||
|
|
||||||
## Support / Donate
|
## Support / Donate
|
||||||
|
|
||||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
|
||||||
Lemmy is made possible by a generous grant from the [NLnet foundation](https://nlnet.nl/).
|
|
||||||
|
|
||||||
- [Support on Liberapay](https://liberapay.com/Lemmy).
|
- [Support on Liberapay](https://liberapay.com/Lemmy).
|
||||||
- [Support on Patreon](https://www.patreon.com/dessalines).
|
- [Support on Patreon](https://www.patreon.com/dessalines).
|
||||||
- [Support on OpenCollective](https://opencollective.com/lemmy).
|
- [Support on OpenCollective](https://opencollective.com/lemmy).
|
||||||
- [List of Sponsors](https://join-lemmy.org/donate).
|
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
||||||
|
|
||||||
### Crypto
|
### Crypto
|
||||||
|
|
||||||
|
@ -136,29 +121,24 @@ Lemmy is made possible by a generous grant from the [NLnet foundation](https://n
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Read the following documentation to setup the development environment and start coding:
|
- [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html)
|
||||||
|
- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
|
||||||
- [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.html)
|
- [Local Development](https://dev.lemmy.ml/docs/contributing_local_development.html)
|
||||||
- [Docker Development](https://join-lemmy.org/docs/contributors/03-docker-development.html)
|
|
||||||
- [Local Development](https://join-lemmy.org/docs/contributors/02-local-development.html)
|
|
||||||
|
|
||||||
When working on an issue or pull request, you can comment with any questions you may have so that maintainers can answer them. You can also join the [Matrix Development Chat](https://matrix.to/#/#lemmydev:matrix.org) for general assistance.
|
|
||||||
|
|
||||||
### Translations
|
### Translations
|
||||||
|
|
||||||
- If you want to help with translating, take a look at [Weblate](https://weblate.join-lemmy.org/projects/lemmy/). You can also help by [translating the documentation](https://github.com/LemmyNet/lemmy-docs#adding-a-new-language).
|
If you want to help with translating, take a look at [Weblate](https://weblate.yerbamate.dev/projects/lemmy/).
|
||||||
|
|
||||||
## Community
|
## Contact
|
||||||
|
|
||||||
- [Matrix Space](https://matrix.to/#/#lemmy-space:matrix.org)
|
- [Mastodon](https://mastodon.social/@LemmyDev)
|
||||||
- [Lemmy Forum](https://lemmy.ml/c/lemmy)
|
- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
|
||||||
- [Lemmy Support Forum](https://lemmy.ml/c/lemmy_support)
|
|
||||||
|
|
||||||
## Code Mirrors
|
## Code Mirrors
|
||||||
|
|
||||||
- [GitHub](https://github.com/LemmyNet/lemmy)
|
- [GitHub](https://github.com/LemmyNet/lemmy)
|
||||||
- [Gitea](https://git.join-lemmy.org/LemmyNet/lemmy)
|
- [Gitea](https://yerbamate.dev/LemmyNet/lemmy)
|
||||||
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
|
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|
86
RELEASES.md
vendored
86
RELEASES.md
vendored
|
@ -1,3 +1,85 @@
|
||||||
[Lemmy Releases / news](https://join-lemmy.org/news)
|
# Lemmy v0.7.0 Release (2020-06-23)
|
||||||
|
|
||||||
[Github link](https://github.com/LemmyNet/joinlemmy-site/tree/main/src/assets/news)
|
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
|
||||||
|
with [pict-rs](https://git.asonix.dog/asonix/pict-rs), which improves performance
|
||||||
|
and security.
|
||||||
|
|
||||||
|
Overall, since our last major release in January (v0.6.0), we have closed over
|
||||||
|
[100 issues!](https://github.com/LemmyNet/lemmy/milestone/16?closed=1)
|
||||||
|
|
||||||
|
- Site-wide list of recent comments
|
||||||
|
- Reconnecting websockets
|
||||||
|
- Many more themes, including a default light one.
|
||||||
|
- Expandable embeds for post links (and thumbnails), from
|
||||||
|
[iframely](https://github.com/itteco/iframely)
|
||||||
|
- Better icons
|
||||||
|
- Emoji autocomplete to post and message bodies, and an Emoji Picker
|
||||||
|
- Post body now searchable
|
||||||
|
- Community title and description is now searchable
|
||||||
|
- Simplified cross-posts
|
||||||
|
- Better documentation
|
||||||
|
- LOTS more languages
|
||||||
|
- Lots of bugs squashed
|
||||||
|
- And more ...
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
Before starting the upgrade, make sure that you have a working backup of your
|
||||||
|
database and image files. See our
|
||||||
|
[documentation](https://dev.lemmy.ml/docs/administration_backup_and_restore.html)
|
||||||
|
for backup instructions.
|
||||||
|
|
||||||
|
**With Ansible:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# deploy with ansible from your local lemmy git repo
|
||||||
|
git pull
|
||||||
|
cd ansible
|
||||||
|
ansible-playbook lemmy.yml
|
||||||
|
# connect via ssh to run the migration script
|
||||||
|
ssh your-server
|
||||||
|
cd /lemmy/
|
||||||
|
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
|
||||||
|
chmod +x migrate-pictshare-to-pictrs.bash
|
||||||
|
sudo ./migrate-pictshare-to-pictrs.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
**With manual Docker installation:**
|
||||||
|
```
|
||||||
|
# run these commands on your server
|
||||||
|
cd /lemmy
|
||||||
|
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
|
||||||
|
# Replace the {{ vars }}
|
||||||
|
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||||
|
sudo nginx -s reload
|
||||||
|
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
||||||
|
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
|
||||||
|
chmod +x migrate-pictshare-to-pictrs.bash
|
||||||
|
sudo bash migrate-pictshare-to-pictrs.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** After upgrading, all users need to reload the page, then logout and
|
||||||
|
login again, so that images are loaded correctly.
|
||||||
|
|
||||||
|
# Lemmy v0.6.0 Release (2020-01-16)
|
||||||
|
|
||||||
|
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)
|
||||||
|
|
||||||
|
This is the biggest release by far:
|
||||||
|
|
||||||
|
- Avatars!
|
||||||
|
- Optional Email notifications for username mentions, post and comment replies.
|
||||||
|
- Ability to change your password and email address.
|
||||||
|
- Can set a custom language.
|
||||||
|
- Lemmy-wide settings to disable downvotes, and close registration.
|
||||||
|
- A better documentation system, hosted in lemmy itself.
|
||||||
|
- [Huge DB performance gains](https://github.com/LemmyNet/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
|
||||||
|
- Fixed major issue with similar post URL and title searching.
|
||||||
|
- Upgraded to Actix `2.0`
|
||||||
|
- Faster comment / post voting.
|
||||||
|
- Better small screen support.
|
||||||
|
- Lots of bug fixes, refactoring of back end code.
|
||||||
|
|
||||||
|
Another major announcement is that Lemmy now has another lead developer besides me, [@felix@radical.town](https://radical.town/@felix). Theyve created a better documentation system, implemented RSS feeds, simplified docker and project configs, upgraded actix, working on federation, a whole lot else.
|
||||||
|
|
||||||
|
https://dev.lemmy.ml
|
||||||
|
|
5
SECURITY.md
vendored
5
SECURITY.md
vendored
|
@ -1,5 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Use [Github's security advisory issue system](https://github.com/LemmyNet/lemmy/security/advisories/new).
|
|
1
ansible/VERSION
vendored
Normal file
1
ansible/VERSION
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v0.7.1
|
6
ansible/ansible.cfg
vendored
Normal file
6
ansible/ansible.cfg
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[defaults]
|
||||||
|
inventory=inventory
|
||||||
|
interpreter_python=/usr/bin/python3
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
6
ansible/inventory.example
vendored
Normal file
6
ansible/inventory.example
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[lemmy]
|
||||||
|
# define the username and hostname that you use for ssh connection, and specify the domain
|
||||||
|
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com
|
||||||
|
|
||||||
|
[all:vars]
|
||||||
|
ansible_connection=ssh
|
73
ansible/lemmy.yml
vendored
Normal file
73
ansible/lemmy.yml
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
|
||||||
|
# Install python if required
|
||||||
|
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
|
||||||
|
gather_facts: False
|
||||||
|
pre_tasks:
|
||||||
|
- name: install python for Ansible
|
||||||
|
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: output
|
||||||
|
changed_when: output.stdout != ""
|
||||||
|
- setup: # gather facts
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: install dependencies
|
||||||
|
apt:
|
||||||
|
pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
|
||||||
|
|
||||||
|
- name: request initial letsencrypt certificate
|
||||||
|
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
|
||||||
|
args:
|
||||||
|
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||||
|
|
||||||
|
- name: create lemmy folder
|
||||||
|
file: path={{item.path}} {{item.owner}} state=directory
|
||||||
|
with_items:
|
||||||
|
- { path: '/lemmy/', owner: 'root' }
|
||||||
|
- { path: '/lemmy/volumes/', owner: 'root' }
|
||||||
|
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: add template files
|
||||||
|
template: src={{item.src}} dest={{item.dest}} mode={{item.mode}}
|
||||||
|
with_items:
|
||||||
|
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||||
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||||
|
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||||
|
vars:
|
||||||
|
lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}"
|
||||||
|
lemmy_port: "8536"
|
||||||
|
pictshare_port: "8537"
|
||||||
|
iframely_port: "8538"
|
||||||
|
|
||||||
|
- name: add config file (only during initial setup)
|
||||||
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
vars:
|
||||||
|
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
||||||
|
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
||||||
|
|
||||||
|
- name: enable and start docker service
|
||||||
|
systemd:
|
||||||
|
name: docker
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: start docker-compose
|
||||||
|
docker_compose:
|
||||||
|
project_src: /lemmy/
|
||||||
|
state: present
|
||||||
|
pull: yes
|
||||||
|
remove_orphans: yes
|
||||||
|
|
||||||
|
- name: reload nginx with new config
|
||||||
|
shell: nginx -s reload
|
||||||
|
|
||||||
|
- name: certbot renewal cronjob
|
||||||
|
cron:
|
||||||
|
special_time=daily
|
||||||
|
name=certbot-renew-lemmy
|
||||||
|
user=root
|
||||||
|
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
|
103
ansible/lemmy_dev.yml
vendored
Normal file
103
ansible/lemmy_dev.yml
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
vars:
|
||||||
|
lemmy_docker_image: "lemmy:dev"
|
||||||
|
|
||||||
|
# Install python if required
|
||||||
|
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
|
||||||
|
gather_facts: False
|
||||||
|
pre_tasks:
|
||||||
|
- name: install python for Ansible
|
||||||
|
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: output
|
||||||
|
changed_when: output.stdout != ""
|
||||||
|
- setup: # gather facts
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: install dependencies
|
||||||
|
apt:
|
||||||
|
pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
|
||||||
|
|
||||||
|
- name: request initial letsencrypt certificate
|
||||||
|
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
|
||||||
|
args:
|
||||||
|
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||||
|
|
||||||
|
- name: create lemmy folder
|
||||||
|
file: path={{item.path}} owner={{item.owner}} state=directory
|
||||||
|
with_items:
|
||||||
|
- { path: '/lemmy/', owner: 'root' }
|
||||||
|
- { path: '/lemmy/volumes/', owner: 'root' }
|
||||||
|
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: add template files
|
||||||
|
template: src={{item.src}} dest={{item.dest}} mode={{item.mode}}
|
||||||
|
with_items:
|
||||||
|
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
|
||||||
|
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
|
||||||
|
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
|
||||||
|
|
||||||
|
- name: add config file (only during initial setup)
|
||||||
|
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
|
||||||
|
vars:
|
||||||
|
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
|
||||||
|
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
|
||||||
|
|
||||||
|
- name: build the dev docker image
|
||||||
|
local_action: shell cd .. && sudo docker build . -f docker/dev/Dockerfile -t lemmy:dev
|
||||||
|
register: image_build
|
||||||
|
|
||||||
|
- name: find hash of the new docker image
|
||||||
|
set_fact:
|
||||||
|
image_hash: "{{ image_build.stdout | regex_search('(?<=Successfully built )[0-9a-f]{12}') }}"
|
||||||
|
|
||||||
|
# this does not use become so that the output file is written as non-root user and is easy to delete later
|
||||||
|
- name: save dev docker image to file
|
||||||
|
local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar
|
||||||
|
|
||||||
|
- name: copy dev docker image to server
|
||||||
|
copy: src=lemmy-dev.tar dest=/lemmy/lemmy-dev.tar
|
||||||
|
|
||||||
|
- name: import docker image
|
||||||
|
docker_image:
|
||||||
|
name: lemmy
|
||||||
|
tag: dev
|
||||||
|
load_path: /lemmy/lemmy-dev.tar
|
||||||
|
source: load
|
||||||
|
force_source: yes
|
||||||
|
register: image_import
|
||||||
|
|
||||||
|
- name: delete remote image file
|
||||||
|
file: path=/lemmy/lemmy-dev.tar state=absent
|
||||||
|
|
||||||
|
- name: delete local image file
|
||||||
|
local_action: file path=lemmy-dev.tar state=absent
|
||||||
|
|
||||||
|
- name: enable and start docker service
|
||||||
|
systemd:
|
||||||
|
name: docker
|
||||||
|
enabled: yes
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# cant pull here because that fails due to lemmy:dev (without dessalines/) not being on docker hub, but that shouldnt
|
||||||
|
# be a problem for testing
|
||||||
|
- name: start docker-compose
|
||||||
|
docker_compose:
|
||||||
|
project_src: /lemmy/
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
remove_orphans: yes
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: reload nginx with new config
|
||||||
|
shell: nginx -s reload
|
||||||
|
|
||||||
|
- name: certbot renewal cronjob
|
||||||
|
cron:
|
||||||
|
special_time=daily
|
||||||
|
name=certbot-renew-lemmy
|
||||||
|
user=root
|
||||||
|
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"
|
14
ansible/templates/config.hjson
vendored
Normal file
14
ansible/templates/config.hjson
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
database: {
|
||||||
|
password: "{{ postgres_password }}"
|
||||||
|
host: "postgres"
|
||||||
|
}
|
||||||
|
hostname: "{{ domain }}"
|
||||||
|
jwt_secret: "{{ jwt_password }}"
|
||||||
|
front_end_dir: "/app/dist"
|
||||||
|
email: {
|
||||||
|
smtp_server: "postfix:25"
|
||||||
|
smtp_from_address: "noreply@{{ domain }}"
|
||||||
|
use_tls: false
|
||||||
|
}
|
||||||
|
}
|
49
ansible/templates/docker-compose.yml
vendored
Normal file
49
ansible/templates/docker-compose.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
lemmy:
|
||||||
|
image: {{ lemmy_docker_image }}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8536:8536"
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- RUST_LOG=error
|
||||||
|
volumes:
|
||||||
|
- ./lemmy.hjson:/config/config.hjson:ro
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- pictrs
|
||||||
|
- iframely
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD={{ postgres_password }}
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
pictrs:
|
||||||
|
image: asonix/pictrs:amd64-v0.1.0-r9
|
||||||
|
user: 991:991
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8537:8080"
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pictrs:/mnt
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8061:80"
|
||||||
|
volumes:
|
||||||
|
- ./iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
postfix:
|
||||||
|
image: mwader/postfix-relay
|
||||||
|
environment:
|
||||||
|
- POSTFIX_myhostname={{ domain }}
|
||||||
|
restart: "always"
|
109
ansible/templates/nginx.conf
vendored
Normal file
109
ansible/templates/nginx.conf
vendored
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name {{ domain }};
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name {{ domain }};
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/{{domain}}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/{{domain}}/privkey.pem;
|
||||||
|
|
||||||
|
# Various TLS hardening settings
|
||||||
|
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
|
||||||
|
# Hide nginx version
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# Enable compression for JS/CSS/HTML bundle, for improved client load times.
|
||||||
|
# It might be nice to compress JSON, but leaving that out to protect against potential
|
||||||
|
# compression+encryption information leak attacks like BREACH.
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript image/svg+xml;
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
# Only connect to this site via HTTPS for the two years
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000";
|
||||||
|
|
||||||
|
# Various content security headers
|
||||||
|
add_header Referrer-Policy "same-origin";
|
||||||
|
add_header X-Content-Type-Options "nosniff";
|
||||||
|
add_header X-Frame-Options "DENY";
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
|
||||||
|
# Upload limit for pictrs
|
||||||
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://0.0.0.0:8536;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Proxy Cache
|
||||||
|
proxy_cache lemmy_frontend_cache;
|
||||||
|
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_revalidate on;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
proxy_cache_min_uses 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect pictshare images to pictrs
|
||||||
|
location ~ /pictshare/(.*)$ {
|
||||||
|
return 301 /pictrs/image/$1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# pict-rs images
|
||||||
|
location /pictrs {
|
||||||
|
location /pictrs/image {
|
||||||
|
proxy_pass http://0.0.0.0:8537/image;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
# Block the import
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /iframely/ {
|
||||||
|
proxy_pass http://0.0.0.0:8061/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anonymize IP addresses
|
||||||
|
# https://www.supertechcrew.com/anonymizing-logs-nginx-apache/
|
||||||
|
map $remote_addr $remote_addr_anon {
|
||||||
|
~(?P<ip>\d+\.\d+\.\d+)\. $ip.0;
|
||||||
|
~(?P<ip>[^:]+:[^:]+): $ip::;
|
||||||
|
127.0.0.1 $remote_addr;
|
||||||
|
::1 $remote_addr;
|
||||||
|
default 0.0.0.0;
|
||||||
|
}
|
||||||
|
log_format main '$remote_addr_anon - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
|
||||||
|
access_log /var/log/nginx/access.log main;
|
48
ansible/uninstall.yml
vendored
Normal file
48
ansible/uninstall.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
|
||||||
|
vars_prompt:
|
||||||
|
|
||||||
|
- name: confirm_uninstall
|
||||||
|
prompt: "Do you really want to uninstall Lemmy? This will delete all data and can not be reverted [yes/no]"
|
||||||
|
private: no
|
||||||
|
|
||||||
|
- name: delete_certs
|
||||||
|
prompt: "Delete certificates? Select 'no' if you want to reinstall Lemmy [yes/no]"
|
||||||
|
private: no
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: end play if no confirmation was given
|
||||||
|
debug:
|
||||||
|
msg: "Uninstall cancelled, doing nothing"
|
||||||
|
when: not confirm_uninstall|bool
|
||||||
|
|
||||||
|
- meta: end_play
|
||||||
|
when: not confirm_uninstall|bool
|
||||||
|
|
||||||
|
- name: stop docker-compose
|
||||||
|
docker_compose:
|
||||||
|
project_src: /lemmy/
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: delete data
|
||||||
|
file: path={{item.path}} state=absent
|
||||||
|
with_items:
|
||||||
|
- { path: '/lemmy/' }
|
||||||
|
- { path: '/etc/nginx/sites-enabled/lemmy.conf' }
|
||||||
|
|
||||||
|
- name: Remove a volume
|
||||||
|
docker_volume: name={{item.name}} state=absent
|
||||||
|
with_items:
|
||||||
|
- { name: 'lemmy_lemmy_db' }
|
||||||
|
- { name: 'lemmy_lemmy_pictshare' }
|
||||||
|
|
||||||
|
- name: delete entire ecloud folder
|
||||||
|
file: path='/mnt/repo-base/' state=absent
|
||||||
|
when: delete_certs|bool
|
||||||
|
|
||||||
|
- name: remove certbot cronjob
|
||||||
|
cron:
|
||||||
|
name=certbot-renew-lemmy
|
||||||
|
state=absent
|
||||||
|
|
1
api_tests/.npmrc
vendored
1
api_tests/.npmrc
vendored
|
@ -1 +0,0 @@
|
||||||
package-manager-strict=false
|
|
4
api_tests/.prettierrc.json
vendored
4
api_tests/.prettierrc.json
vendored
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"semi": true
|
|
||||||
}
|
|
56
api_tests/eslint.config.mjs
vendored
56
api_tests/eslint.config.mjs
vendored
|
@ -1,56 +0,0 @@
|
||||||
import pluginJs from "@eslint/js";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
pluginJs.configs.recommended,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
parser: tseslint.parser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// For some reason this has to be in its own block
|
|
||||||
{
|
|
||||||
ignores: [
|
|
||||||
"putTypesInIndex.js",
|
|
||||||
"dist/*",
|
|
||||||
"docs/*",
|
|
||||||
".yalc",
|
|
||||||
"jest.config.js",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/**/*"],
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-empty-interface": 0,
|
|
||||||
"@typescript-eslint/no-empty-function": 0,
|
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
||||||
"@typescript-eslint/no-var-requires": 0,
|
|
||||||
"arrow-body-style": 0,
|
|
||||||
curly: 0,
|
|
||||||
"eol-last": 0,
|
|
||||||
eqeqeq: 0,
|
|
||||||
"func-style": 0,
|
|
||||||
"import/no-duplicates": 0,
|
|
||||||
"max-statements": 0,
|
|
||||||
"max-params": 0,
|
|
||||||
"new-cap": 0,
|
|
||||||
"no-console": 0,
|
|
||||||
"no-duplicate-imports": 0,
|
|
||||||
"no-extra-parens": 0,
|
|
||||||
"no-return-assign": 0,
|
|
||||||
"no-throw-literal": 0,
|
|
||||||
"no-trailing-spaces": 0,
|
|
||||||
"no-unused-expressions": 0,
|
|
||||||
"no-useless-constructor": 0,
|
|
||||||
"no-useless-escape": 0,
|
|
||||||
"no-var": 0,
|
|
||||||
"prefer-const": 0,
|
|
||||||
"prefer-rest-params": 0,
|
|
||||||
"quote-props": 0,
|
|
||||||
"unicorn/filename-case": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
4
api_tests/jest.config.js
vendored
4
api_tests/jest.config.js
vendored
|
@ -1,4 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "node",
|
|
||||||
};
|
|
37
api_tests/package.json
vendored
37
api_tests/package.json
vendored
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"name": "api_tests",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "API tests for lemmy backend",
|
|
||||||
"main": "index.js",
|
|
||||||
"repository": "https://github.com/LemmyNet/lemmy",
|
|
||||||
"author": "Dessalines",
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"packageManager": "pnpm@9.15.0",
|
|
||||||
"scripts": {
|
|
||||||
"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 image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i private_community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
|
|
||||||
"api-test-follow": "jest -i follow.spec.ts",
|
|
||||||
"api-test-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-community": "jest -i private_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.12",
|
|
||||||
"@types/node": "^22.10.6",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
||||||
"@typescript-eslint/parser": "^8.20.0",
|
|
||||||
"eslint": "^9.18.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"jest": "^29.5.0",
|
|
||||||
"lemmy-js-client": "0.20.0-modlog-combined.0",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"ts-jest": "^29.1.0",
|
|
||||||
"typescript": "^5.7.3",
|
|
||||||
"typescript-eslint": "^8.20.0"
|
|
||||||
}
|
|
||||||
}
|
|
3440
api_tests/pnpm-lock.yaml
vendored
3440
api_tests/pnpm-lock.yaml
vendored
File diff suppressed because it is too large
Load diff
96
api_tests/prepare-drone-federation-test.sh
vendored
96
api_tests/prepare-drone-federation-test.sh
vendored
|
@ -1,96 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# IMPORTANT NOTE: this script does not use the normal LEMMY_DATABASE_URL format
|
|
||||||
# it is expected that this script is called by run-federation-test.sh script.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$LEMMY_LOG_LEVEL" ];
|
|
||||||
then
|
|
||||||
LEMMY_LOG_LEVEL=info
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RUST_BACKTRACE=1
|
|
||||||
export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
|
|
||||||
|
|
||||||
export 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
|
|
||||||
# This one sometimes goes down
|
|
||||||
# curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs
|
|
||||||
curl "https://codeberg.org/asonix/pict-rs/releases/download/v0.5.6/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"
|
|
||||||
echo "create database"
|
|
||||||
psql "${LEMMY_DATABASE_URL}/lemmy" -c "CREATE DATABASE $INSTANCE"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$DO_WRITE_HOSTS_FILE" ]; then
|
|
||||||
if ! grep -q lemmy-alpha /etc/hosts; then
|
|
||||||
echo "Please add the following to your /etc/hosts file, then press enter:
|
|
||||||
|
|
||||||
127.0.0.1 lemmy-alpha
|
|
||||||
127.0.0.1 lemmy-beta
|
|
||||||
127.0.0.1 lemmy-gamma
|
|
||||||
127.0.0.1 lemmy-delta
|
|
||||||
127.0.0.1 lemmy-epsilon"
|
|
||||||
read -p ""
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
for INSTANCE in lemmy-alpha lemmy-beta lemmy-gamma lemmy-delta lemmy-epsilon; do
|
|
||||||
echo "127.0.0.1 $INSTANCE" >>/etc/hosts
|
|
||||||
done
|
|
||||||
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 >$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 >$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 >$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 >$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 >$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/v4/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "alpha started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v4/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "beta started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v4/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "gamma started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v4/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "delta started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v4/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "epsilon started. All started"
|
|
21
api_tests/run-federation-test.sh
vendored
21
api_tests/run-federation-test.sh
vendored
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432
|
|
||||||
pushd ..
|
|
||||||
cargo build
|
|
||||||
rm target/lemmy_server || true
|
|
||||||
cp target/debug/lemmy_server target/lemmy_server
|
|
||||||
killall -s1 lemmy_server || true
|
|
||||||
./api_tests/prepare-drone-federation-test.sh
|
|
||||||
popd
|
|
||||||
|
|
||||||
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
|
|
900
api_tests/src/comment.spec.ts
vendored
900
api_tests/src/comment.spec.ts
vendored
|
@ -1,900 +0,0 @@
|
||||||
jest.setTimeout(180000);
|
|
||||||
|
|
||||||
import { PostResponse } from "lemmy-js-client/dist/types/PostResponse";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
gamma,
|
|
||||||
setupLogins,
|
|
||||||
createPost,
|
|
||||||
getPost,
|
|
||||||
resolveComment,
|
|
||||||
likeComment,
|
|
||||||
followBeta,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
createComment,
|
|
||||||
editComment,
|
|
||||||
deleteComment,
|
|
||||||
removeComment,
|
|
||||||
getMentions,
|
|
||||||
resolvePost,
|
|
||||||
unfollowRemotes,
|
|
||||||
createCommunity,
|
|
||||||
registerUser,
|
|
||||||
reportComment,
|
|
||||||
randomString,
|
|
||||||
unfollows,
|
|
||||||
getComments,
|
|
||||||
getCommentParentId,
|
|
||||||
resolveCommunity,
|
|
||||||
getReplies,
|
|
||||||
getUnreadCount,
|
|
||||||
waitUntil,
|
|
||||||
waitForPost,
|
|
||||||
alphaUrl,
|
|
||||||
followCommunity,
|
|
||||||
blockCommunity,
|
|
||||||
delay,
|
|
||||||
saveUserSettings,
|
|
||||||
listReports,
|
|
||||||
listPersonContent,
|
|
||||||
} from "./shared";
|
|
||||||
import {
|
|
||||||
CommentReportView,
|
|
||||||
CommentView,
|
|
||||||
CommunityView,
|
|
||||||
ReportCombinedView,
|
|
||||||
SaveUserSettings,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
|
|
||||||
let betaCommunity: CommunityView | undefined;
|
|
||||||
let postOnAlphaRes: PostResponse;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await setupLogins();
|
|
||||||
await Promise.all([followBeta(alpha), followBeta(gamma)]);
|
|
||||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
|
||||||
if (betaCommunity) {
|
|
||||||
postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
function assertCommentFederation(
|
|
||||||
commentOne?: CommentView,
|
|
||||||
commentTwo?: CommentView,
|
|
||||||
) {
|
|
||||||
expect(commentOne?.comment.ap_id).toBe(commentTwo?.comment.ap_id);
|
|
||||||
expect(commentOne?.comment.content).toBe(commentTwo?.comment.content);
|
|
||||||
expect(commentOne?.creator.name).toBe(commentTwo?.creator.name);
|
|
||||||
expect(commentOne?.community.actor_id).toBe(commentTwo?.community.actor_id);
|
|
||||||
expect(commentOne?.comment.published).toBe(commentTwo?.comment.published);
|
|
||||||
expect(commentOne?.comment.updated).toBe(commentOne?.comment.updated);
|
|
||||||
expect(commentOne?.comment.deleted).toBe(commentOne?.comment.deleted);
|
|
||||||
expect(commentOne?.comment.removed).toBe(commentOne?.comment.removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Create a comment", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
|
||||||
expect(commentRes.comment_view.community.local).toBe(false);
|
|
||||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
|
||||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure that comment is liked on beta
|
|
||||||
let betaComment = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 1,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
expect(betaComment).toBeDefined();
|
|
||||||
expect(betaComment?.community.local).toBe(true);
|
|
||||||
expect(betaComment?.creator.local).toBe(false);
|
|
||||||
expect(betaComment?.counts.score).toBe(1);
|
|
||||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create a comment in a non-existent post", async () => {
|
|
||||||
await expect(createComment(alpha, -1)).rejects.toStrictEqual(
|
|
||||||
Error("not_found"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Update a comment", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
// Federate the comment first
|
|
||||||
let betaComment = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
|
||||||
|
|
||||||
let updateCommentRes = await editComment(
|
|
||||||
alpha,
|
|
||||||
commentRes.comment_view.comment.id,
|
|
||||||
);
|
|
||||||
expect(updateCommentRes.comment_view.comment.content).toBe(
|
|
||||||
"A jest test federated comment update",
|
|
||||||
);
|
|
||||||
expect(updateCommentRes.comment_view.community.local).toBe(false);
|
|
||||||
expect(updateCommentRes.comment_view.creator.local).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that post is updated on beta
|
|
||||||
let betaCommentUpdated = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c =>
|
|
||||||
c.comment?.comment.content === "A jest test federated comment update",
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
assertCommentFederation(betaCommentUpdated, updateCommentRes.comment_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
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, post.post_view.post.id);
|
|
||||||
|
|
||||||
// Find the comment on beta (home of community)
|
|
||||||
let betaComment = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
if (!betaComment) {
|
|
||||||
throw "Missing beta comment before delete";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the comment on remote instance gamma
|
|
||||||
let gammaComment = (
|
|
||||||
await waitUntil(
|
|
||||||
() =>
|
|
||||||
resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
|
|
||||||
r => r.message !== "not_found",
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
if (!gammaComment) {
|
|
||||||
throw "Missing gamma comment (remote-home-remote replication) before delete";
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleteCommentRes = await deleteComment(
|
|
||||||
alpha,
|
|
||||||
true,
|
|
||||||
commentRes.comment_view.comment.id,
|
|
||||||
);
|
|
||||||
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that comment is deleted on beta
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.comment.deleted === true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure that comment is deleted on gamma after delete
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.comment.deleted === true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test undeleting the comment
|
|
||||||
let undeleteCommentRes = await deleteComment(
|
|
||||||
alpha,
|
|
||||||
false,
|
|
||||||
commentRes.comment_view.comment.id,
|
|
||||||
);
|
|
||||||
expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false);
|
|
||||||
|
|
||||||
// Make sure that comment is undeleted on beta
|
|
||||||
let betaComment2 = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.comment.deleted === false,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.skip("Remove a comment from admin and community on the same instance", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
|
|
||||||
// Get the id for beta
|
|
||||||
let betaCommentId = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment?.comment.id;
|
|
||||||
|
|
||||||
if (!betaCommentId) {
|
|
||||||
throw "beta comment id is missing";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The beta admin removes it (the community lives on beta)
|
|
||||||
let removeCommentRes = await removeComment(beta, true, betaCommentId);
|
|
||||||
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
|
|
||||||
let refetchedPostComments = await listPersonContent(
|
|
||||||
alpha,
|
|
||||||
commentRes.comment_view.comment.creator_id,
|
|
||||||
"Comments",
|
|
||||||
);
|
|
||||||
let firstRefetchedComment = refetchedPostComments.content[0] as CommentView;
|
|
||||||
expect(firstRefetchedComment.comment.removed).toBe(true);
|
|
||||||
|
|
||||||
// beta will unremove the comment
|
|
||||||
let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
|
|
||||||
expect(unremoveCommentRes.comment_view.comment.removed).toBe(false);
|
|
||||||
|
|
||||||
// Make sure that comment is unremoved on alpha
|
|
||||||
let refetchedPostComments2 = await getComments(
|
|
||||||
alpha,
|
|
||||||
postOnAlphaRes.post_view.post.id,
|
|
||||||
);
|
|
||||||
expect(refetchedPostComments2.comments[0].comment.removed).toBe(false);
|
|
||||||
assertCommentFederation(
|
|
||||||
refetchedPostComments2.comments[0],
|
|
||||||
unremoveCommentRes.comment_view,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Remove a comment from admin and community on different instance", async () => {
|
|
||||||
let newAlphaApi = await registerUser(alpha, alphaUrl);
|
|
||||||
|
|
||||||
// New alpha user creates a community, post, and comment.
|
|
||||||
let newCommunity = await createCommunity(newAlphaApi);
|
|
||||||
let newPost = await createPost(
|
|
||||||
newAlphaApi,
|
|
||||||
newCommunity.community_view.community.id,
|
|
||||||
);
|
|
||||||
let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id);
|
|
||||||
expect(commentRes.comment_view.comment.content).toBeDefined();
|
|
||||||
|
|
||||||
// Beta searches that to cache it, then removes it
|
|
||||||
let betaComment = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
|
|
||||||
if (!betaComment) {
|
|
||||||
throw "beta comment missing";
|
|
||||||
}
|
|
||||||
|
|
||||||
let removeCommentRes = await removeComment(
|
|
||||||
beta,
|
|
||||||
true,
|
|
||||||
betaComment.comment.id,
|
|
||||||
);
|
|
||||||
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Make sure its not removed on alpha
|
|
||||||
let refetchedPostComments = await getComments(
|
|
||||||
alpha,
|
|
||||||
newPost.post_view.post.id,
|
|
||||||
);
|
|
||||||
expect(refetchedPostComments.comments[0].comment.removed).toBe(false);
|
|
||||||
assertCommentFederation(
|
|
||||||
refetchedPostComments.comments[0],
|
|
||||||
commentRes.comment_view,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Unlike a comment", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
|
|
||||||
// Lemmy automatically creates 1 like (vote) by author of comment.
|
|
||||||
// Make sure that comment is liked (voted up) on gamma, downstream peer
|
|
||||||
// This is testing replication from remote-home-remote (alpha-beta-gamma)
|
|
||||||
|
|
||||||
let gammaComment1 = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 1,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
expect(gammaComment1).toBeDefined();
|
|
||||||
expect(gammaComment1?.community.local).toBe(false);
|
|
||||||
expect(gammaComment1?.creator.local).toBe(false);
|
|
||||||
expect(gammaComment1?.counts.score).toBe(1);
|
|
||||||
|
|
||||||
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
|
|
||||||
expect(unlike.comment_view.counts.score).toBe(0);
|
|
||||||
|
|
||||||
// Make sure that comment is unliked on beta
|
|
||||||
let betaComment = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 0,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
expect(betaComment).toBeDefined();
|
|
||||||
expect(betaComment?.community.local).toBe(true);
|
|
||||||
expect(betaComment?.creator.local).toBe(false);
|
|
||||||
expect(betaComment?.counts.score).toBe(0);
|
|
||||||
|
|
||||||
// Make sure that comment is unliked on gamma, downstream peer
|
|
||||||
// This is testing replication from remote-home-remote (alpha-beta-gamma)
|
|
||||||
let gammaComment = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(gamma, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 0,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
expect(gammaComment).toBeDefined();
|
|
||||||
expect(gammaComment?.community.local).toBe(false);
|
|
||||||
expect(gammaComment?.creator.local).toBe(false);
|
|
||||||
expect(gammaComment?.counts.score).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Federated comment like", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 1,
|
|
||||||
);
|
|
||||||
// Find the comment on beta
|
|
||||||
let betaComment = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
|
|
||||||
if (!betaComment) {
|
|
||||||
throw "Missing beta comment";
|
|
||||||
}
|
|
||||||
|
|
||||||
let like = await likeComment(beta, 1, betaComment.comment);
|
|
||||||
expect(like.comment_view.counts.score).toBe(2);
|
|
||||||
|
|
||||||
// Get the post from alpha, check the likes
|
|
||||||
let postComments = await waitUntil(
|
|
||||||
() => getComments(alpha, postOnAlphaRes.post_view.post.id),
|
|
||||||
c => c.comments[0].counts.score === 2,
|
|
||||||
);
|
|
||||||
expect(postComments.comments[0].counts.score).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Reply to a comment from another instance, get notification", async () => {
|
|
||||||
await alpha.markAllAsRead();
|
|
||||||
|
|
||||||
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 waitUntil(
|
|
||||||
() => resolveComment(beta, commentRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 1,
|
|
||||||
)
|
|
||||||
).comment;
|
|
||||||
|
|
||||||
if (!betaComment) {
|
|
||||||
throw "Missing beta comment";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reply from beta, extending the branch
|
|
||||||
let replyRes = await createComment(
|
|
||||||
beta,
|
|
||||||
betaComment.post.id,
|
|
||||||
betaComment.comment.id,
|
|
||||||
);
|
|
||||||
expect(replyRes.comment_view.comment.content).toBeDefined();
|
|
||||||
expect(replyRes.comment_view.community.local).toBe(true);
|
|
||||||
expect(replyRes.comment_view.creator.local).toBe(true);
|
|
||||||
expect(getCommentParentId(replyRes.comment_view.comment)).toBe(
|
|
||||||
betaComment.comment.id,
|
|
||||||
);
|
|
||||||
expect(replyRes.comment_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure that reply comment is seen on alpha
|
|
||||||
let commentSearch = await waitUntil(
|
|
||||||
() => resolveComment(alpha, replyRes.comment_view.comment),
|
|
||||||
c => c.comment?.counts.score === 1,
|
|
||||||
);
|
|
||||||
let alphaComment = commentSearch.comment!;
|
|
||||||
let postComments = await waitUntil(
|
|
||||||
() => getComments(alpha, postOnAlphaRes.post_view.post.id),
|
|
||||||
pc => pc.comments.length >= 2,
|
|
||||||
);
|
|
||||||
// Note: this test fails when run twice and this count will differ
|
|
||||||
expect(postComments.comments.length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(alphaComment.comment.content).toBeDefined();
|
|
||||||
|
|
||||||
expect(getCommentParentId(alphaComment.comment)).toBe(
|
|
||||||
postComments.comments[1].comment.id,
|
|
||||||
);
|
|
||||||
expect(alphaComment.community.local).toBe(false);
|
|
||||||
expect(alphaComment.creator.local).toBe(false);
|
|
||||||
expect(alphaComment.counts.score).toBe(1);
|
|
||||||
assertCommentFederation(alphaComment, replyRes.comment_view);
|
|
||||||
|
|
||||||
// Did alpha get notified of the reply from beta?
|
|
||||||
let alphaUnreadCountRes = await waitUntil(
|
|
||||||
() => getUnreadCount(alpha),
|
|
||||||
e => e.replies >= 1,
|
|
||||||
);
|
|
||||||
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
// check inbox of replies on alpha, fetching read/unread both
|
|
||||||
let alphaRepliesRes = await waitUntil(
|
|
||||||
() => getReplies(alpha),
|
|
||||||
r => r.replies.length > 0,
|
|
||||||
);
|
|
||||||
const alphaReply = alphaRepliesRes.replies.find(
|
|
||||||
r => r.comment.id === alphaComment.comment.id,
|
|
||||||
);
|
|
||||||
expect(alphaReply).toBeDefined();
|
|
||||||
if (!alphaReply) throw Error();
|
|
||||||
expect(alphaReply.comment.content).toBeDefined();
|
|
||||||
expect(alphaReply.community.local).toBe(false);
|
|
||||||
expect(alphaReply.creator.local).toBe(false);
|
|
||||||
expect(alphaReply.counts.score).toBe(1);
|
|
||||||
// ToDo: interesting alphaRepliesRes.replies[0].comment_reply.id is 1, meaning? how did that come about?
|
|
||||||
expect(alphaReply.comment.id).toBe(alphaComment.comment.id);
|
|
||||||
// this is a new notification, getReplies fetch was for read/unread both, confirm it is unread.
|
|
||||||
expect(alphaReply.comment_reply.read).toBe(false);
|
|
||||||
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);
|
|
||||||
// Create a new branch, trunk-level comment branch, from alpha instance
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
// Create a reply comment to previous comment, this has a mention in body
|
|
||||||
let mentionContent = "A test mention of @lemmy_beta@lemmy-beta:8551";
|
|
||||||
let mentionRes = await createComment(
|
|
||||||
alpha,
|
|
||||||
postOnAlphaRes.post_view.post.id,
|
|
||||||
commentRes.comment_view.comment.id,
|
|
||||||
mentionContent,
|
|
||||||
);
|
|
||||||
expect(mentionRes.comment_view.comment.content).toBeDefined();
|
|
||||||
expect(mentionRes.comment_view.community.local).toBe(false);
|
|
||||||
expect(mentionRes.comment_view.creator.local).toBe(true);
|
|
||||||
expect(mentionRes.comment_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// get beta's localized copy of the alpha post
|
|
||||||
let betaPost = await waitForPost(beta, postOnAlphaRes.post_view.post);
|
|
||||||
if (!betaPost) {
|
|
||||||
throw "unable to locate post on beta";
|
|
||||||
}
|
|
||||||
expect(betaPost.post.ap_id).toBe(postOnAlphaRes.post_view.post.ap_id);
|
|
||||||
expect(betaPost.post.name).toBe(postOnAlphaRes.post_view.post.name);
|
|
||||||
|
|
||||||
// Make sure that both new comments are seen on beta and have parent/child relationship
|
|
||||||
let betaPostComments = await waitUntil(
|
|
||||||
() => getComments(beta, betaPost!.post.id),
|
|
||||||
c => c.comments[1]?.counts.score === 1,
|
|
||||||
);
|
|
||||||
expect(betaPostComments.comments.length).toEqual(2);
|
|
||||||
// the trunk-branch root comment will be older than the mention reply comment, so index 1
|
|
||||||
let betaRootComment = betaPostComments.comments[1];
|
|
||||||
// the trunk-branch root comment should not have a parent
|
|
||||||
expect(getCommentParentId(betaRootComment.comment)).toBeUndefined();
|
|
||||||
expect(betaRootComment.comment.content).toBeDefined();
|
|
||||||
// the mention reply comment should have parent that points to the branch root level comment
|
|
||||||
expect(getCommentParentId(betaPostComments.comments[0].comment)).toBe(
|
|
||||||
betaPostComments.comments[1].comment.id,
|
|
||||||
);
|
|
||||||
expect(betaRootComment.community.local).toBe(true);
|
|
||||||
expect(betaRootComment.creator.local).toBe(false);
|
|
||||||
expect(betaRootComment.counts.score).toBe(1);
|
|
||||||
assertCommentFederation(betaRootComment, commentRes.comment_view);
|
|
||||||
|
|
||||||
let mentionsRes = await waitUntil(
|
|
||||||
() => getMentions(beta),
|
|
||||||
m => !!m.mentions[0],
|
|
||||||
);
|
|
||||||
expect(mentionsRes.mentions[0].comment.content).toBeDefined();
|
|
||||||
expect(mentionsRes.mentions[0].community.local).toBe(true);
|
|
||||||
expect(mentionsRes.mentions[0].creator.local).toBe(false);
|
|
||||||
expect(mentionsRes.mentions[0].counts.score).toBe(1);
|
|
||||||
// the reply comment with mention should be the most fresh, newest, index 0
|
|
||||||
expect(mentionsRes.mentions[0].person_mention.comment_id).toBe(
|
|
||||||
betaPostComments.comments[0].comment.id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Comment Search", async () => {
|
|
||||||
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
|
|
||||||
let betaComment = (
|
|
||||||
await resolveComment(beta, commentRes.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
assertCommentFederation(betaComment, commentRes.comment_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("A and G subscribe to B (center) A posts, G mentions B, it gets announced to A", async () => {
|
|
||||||
// Create a local post
|
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, "!main@lemmy-alpha:8541"))
|
|
||||||
.community;
|
|
||||||
|
|
||||||
if (!alphaCommunity) {
|
|
||||||
throw "Missing alpha community";
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow community from beta so that it accepts the mention
|
|
||||||
let betaCommunity = await resolveCommunity(
|
|
||||||
beta,
|
|
||||||
alphaCommunity.community.actor_id,
|
|
||||||
);
|
|
||||||
await followCommunity(beta, true, betaCommunity.community!.community.id);
|
|
||||||
|
|
||||||
let alphaPost = await createPost(alpha, alphaCommunity.community.id);
|
|
||||||
expect(alphaPost.post_view.community.local).toBe(true);
|
|
||||||
|
|
||||||
// Make sure gamma sees it
|
|
||||||
let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post))!.post;
|
|
||||||
|
|
||||||
if (!gammaPost) {
|
|
||||||
throw "Missing gamma post";
|
|
||||||
}
|
|
||||||
|
|
||||||
let commentContent =
|
|
||||||
"A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551";
|
|
||||||
let commentRes = await createComment(
|
|
||||||
gamma,
|
|
||||||
gammaPost.post.id,
|
|
||||||
undefined,
|
|
||||||
commentContent,
|
|
||||||
);
|
|
||||||
expect(commentRes.comment_view.comment.content).toBe(commentContent);
|
|
||||||
expect(commentRes.comment_view.community.local).toBe(false);
|
|
||||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
|
||||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure alpha sees it
|
|
||||||
let alphaPostComments2 = await waitUntil(
|
|
||||||
() => getComments(alpha, alphaPost.post_view.post.id),
|
|
||||||
e => e.comments[0]?.counts.score === 1,
|
|
||||||
);
|
|
||||||
expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
|
|
||||||
expect(alphaPostComments2.comments[0].community.local).toBe(true);
|
|
||||||
expect(alphaPostComments2.comments[0].creator.local).toBe(false);
|
|
||||||
expect(alphaPostComments2.comments[0].counts.score).toBe(1);
|
|
||||||
assertCommentFederation(
|
|
||||||
alphaPostComments2.comments[0],
|
|
||||||
commentRes.comment_view,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure beta has mentions
|
|
||||||
let relevantMention = await waitUntil(
|
|
||||||
() =>
|
|
||||||
getMentions(beta).then(m =>
|
|
||||||
m.mentions.find(
|
|
||||||
m => m.comment.ap_id === commentRes.comment_view.comment.ap_id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
e => !!e,
|
|
||||||
);
|
|
||||||
if (!relevantMention) throw Error("could not find mention");
|
|
||||||
expect(relevantMention.comment.content).toBe(commentContent);
|
|
||||||
expect(relevantMention.community.local).toBe(false);
|
|
||||||
expect(relevantMention.creator.local).toBe(false);
|
|
||||||
// TODO this is failing because fetchInReplyTos aren't getting score
|
|
||||||
// expect(mentionsRes.mentions[0].score).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Check that activity from another instance is sent to third instance", async () => {
|
|
||||||
// Alpha and gamma users follow beta community
|
|
||||||
let alphaFollow = await followBeta(alpha);
|
|
||||||
expect(alphaFollow.community_view.community.local).toBe(false);
|
|
||||||
expect(alphaFollow.community_view.community.name).toBe("main");
|
|
||||||
|
|
||||||
let gammaFollow = await followBeta(gamma);
|
|
||||||
expect(gammaFollow.community_view.community.local).toBe(false);
|
|
||||||
expect(gammaFollow.community_view.community.name).toBe("main");
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveBetaCommunity(alpha),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
);
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveBetaCommunity(gamma),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a post on beta
|
|
||||||
let betaPost = await createPost(beta, 2);
|
|
||||||
expect(betaPost.post_view.community.local).toBe(true);
|
|
||||||
|
|
||||||
// Make sure gamma and alpha see it
|
|
||||||
let gammaPost = await waitForPost(gamma, betaPost.post_view.post);
|
|
||||||
if (!gammaPost) {
|
|
||||||
throw "Missing gamma post";
|
|
||||||
}
|
|
||||||
expect(gammaPost.post).toBeDefined();
|
|
||||||
|
|
||||||
let alphaPost = await waitForPost(alpha, betaPost.post_view.post);
|
|
||||||
if (!alphaPost) {
|
|
||||||
throw "Missing alpha post";
|
|
||||||
}
|
|
||||||
expect(alphaPost.post).toBeDefined();
|
|
||||||
|
|
||||||
// The bug: gamma comments, and alpha should see it.
|
|
||||||
let commentContent = "Comment from gamma";
|
|
||||||
let commentRes = await createComment(
|
|
||||||
gamma,
|
|
||||||
gammaPost.post.id,
|
|
||||||
undefined,
|
|
||||||
commentContent,
|
|
||||||
);
|
|
||||||
expect(commentRes.comment_view.comment.content).toBe(commentContent);
|
|
||||||
expect(commentRes.comment_view.community.local).toBe(false);
|
|
||||||
expect(commentRes.comment_view.creator.local).toBe(true);
|
|
||||||
expect(commentRes.comment_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure alpha sees it
|
|
||||||
let alphaPostComments2 = await waitUntil(
|
|
||||||
() => getComments(alpha, alphaPost!.post.id),
|
|
||||||
e => e.comments[0]?.counts.score === 1,
|
|
||||||
);
|
|
||||||
expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
|
|
||||||
expect(alphaPostComments2.comments[0].community.local).toBe(false);
|
|
||||||
expect(alphaPostComments2.comments[0].creator.local).toBe(false);
|
|
||||||
expect(alphaPostComments2.comments[0].counts.score).toBe(1);
|
|
||||||
assertCommentFederation(
|
|
||||||
alphaPostComments2.comments[0],
|
|
||||||
commentRes.comment_view,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all([unfollowRemotes(alpha), unfollowRemotes(gamma)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.", async () => {
|
|
||||||
// Unfollow all remote communities
|
|
||||||
let my_user = await unfollowRemotes(alpha);
|
|
||||||
expect(my_user.follows.filter(c => c.community.local == false).length).toBe(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// B creates a post, and two comments, should be invisible to A
|
|
||||||
let postOnBetaRes = await createPost(beta, 2);
|
|
||||||
expect(postOnBetaRes.post_view.post.name).toBeDefined();
|
|
||||||
|
|
||||||
let parentCommentContent = "An invisible top level comment from beta";
|
|
||||||
let parentCommentRes = await createComment(
|
|
||||||
beta,
|
|
||||||
postOnBetaRes.post_view.post.id,
|
|
||||||
undefined,
|
|
||||||
parentCommentContent,
|
|
||||||
);
|
|
||||||
expect(parentCommentRes.comment_view.comment.content).toBe(
|
|
||||||
parentCommentContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// B creates a comment, then a child one of that.
|
|
||||||
let childCommentContent = "An invisible child comment from beta";
|
|
||||||
let childCommentRes = await createComment(
|
|
||||||
beta,
|
|
||||||
postOnBetaRes.post_view.post.id,
|
|
||||||
parentCommentRes.comment_view.comment.id,
|
|
||||||
childCommentContent,
|
|
||||||
);
|
|
||||||
expect(childCommentRes.comment_view.comment.content).toBe(
|
|
||||||
childCommentContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Follow beta again
|
|
||||||
let follow = await followBeta(alpha);
|
|
||||||
expect(follow.community_view.community.local).toBe(false);
|
|
||||||
expect(follow.community_view.community.name).toBe("main");
|
|
||||||
|
|
||||||
// An update to the child comment on beta, should push the post, parent, and child to alpha now
|
|
||||||
let updatedCommentContent = "An update child comment from beta";
|
|
||||||
let updateRes = await editComment(
|
|
||||||
beta,
|
|
||||||
childCommentRes.comment_view.comment.id,
|
|
||||||
updatedCommentContent,
|
|
||||||
);
|
|
||||||
expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent);
|
|
||||||
|
|
||||||
// Get the post from alpha
|
|
||||||
let alphaPostB = await waitForPost(alpha, postOnBetaRes.post_view.post);
|
|
||||||
|
|
||||||
if (!alphaPostB) {
|
|
||||||
throw "Missing alpha post B";
|
|
||||||
}
|
|
||||||
|
|
||||||
let alphaPost = await getPost(alpha, alphaPostB.post.id);
|
|
||||||
let alphaPostComments = await waitUntil(
|
|
||||||
() => getComments(alpha, alphaPostB!.post.id),
|
|
||||||
c =>
|
|
||||||
c.comments[1]?.comment.content ===
|
|
||||||
parentCommentRes.comment_view.comment.content &&
|
|
||||||
c.comments[0]?.comment.content === updateRes.comment_view.comment.content,
|
|
||||||
);
|
|
||||||
expect(alphaPost.post_view.post.name).toBeDefined();
|
|
||||||
assertCommentFederation(
|
|
||||||
alphaPostComments.comments[1],
|
|
||||||
parentCommentRes.comment_view,
|
|
||||||
);
|
|
||||||
assertCommentFederation(
|
|
||||||
alphaPostComments.comments[0],
|
|
||||||
updateRes.comment_view,
|
|
||||||
);
|
|
||||||
expect(alphaPost.post_view.community.local).toBe(false);
|
|
||||||
expect(alphaPost.post_view.creator.local).toBe(false);
|
|
||||||
|
|
||||||
await unfollowRemotes(alpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Report a comment", async () => {
|
|
||||||
let betaCommunity = (await resolveBetaCommunity(beta)).community;
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let postOnBetaRes = (await createPost(beta, betaCommunity.community.id))
|
|
||||||
.post_view.post;
|
|
||||||
expect(postOnBetaRes).toBeDefined();
|
|
||||||
let commentRes = (await createComment(beta, postOnBetaRes.id)).comment_view
|
|
||||||
.comment;
|
|
||||||
expect(commentRes).toBeDefined();
|
|
||||||
|
|
||||||
let alphaComment = (await resolveComment(alpha, commentRes)).comment?.comment;
|
|
||||||
if (!alphaComment) {
|
|
||||||
throw "Missing alpha comment";
|
|
||||||
}
|
|
||||||
|
|
||||||
const reason = randomString(10);
|
|
||||||
let alphaReport = (await reportComment(alpha, alphaComment.id, reason))
|
|
||||||
.comment_report_view.comment_report;
|
|
||||||
|
|
||||||
let betaReport = (
|
|
||||||
(await waitUntil(
|
|
||||||
() =>
|
|
||||||
listReports(beta).then(p =>
|
|
||||||
p.reports.find(r => {
|
|
||||||
return checkCommentReportReason(r, reason);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
e => !!e,
|
|
||||||
)!) as CommentReportView
|
|
||||||
).comment_report;
|
|
||||||
expect(betaReport).toBeDefined();
|
|
||||||
expect(betaReport.resolved).toBe(false);
|
|
||||||
expect(betaReport.original_comment_text).toBe(
|
|
||||||
alphaReport.original_comment_text,
|
|
||||||
);
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkCommentReportReason(rcv: ReportCombinedView, reason: string) {
|
|
||||||
switch (rcv.type_) {
|
|
||||||
case "Comment":
|
|
||||||
return rcv.comment_report.reason === reason;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
604
api_tests/src/community.spec.ts
vendored
604
api_tests/src/community.spec.ts
vendored
|
@ -1,604 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import { AddModToCommunity } from "lemmy-js-client/dist/types/AddModToCommunity";
|
|
||||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
gamma,
|
|
||||||
setupLogins,
|
|
||||||
resolveCommunity,
|
|
||||||
createCommunity,
|
|
||||||
deleteCommunity,
|
|
||||||
delay,
|
|
||||||
removeCommunity,
|
|
||||||
getCommunity,
|
|
||||||
followCommunity,
|
|
||||||
banPersonFromCommunity,
|
|
||||||
resolvePerson,
|
|
||||||
createPost,
|
|
||||||
getPost,
|
|
||||||
resolvePost,
|
|
||||||
registerUser,
|
|
||||||
getPosts,
|
|
||||||
getComments,
|
|
||||||
createComment,
|
|
||||||
getCommunityByName,
|
|
||||||
waitUntil,
|
|
||||||
alphaUrl,
|
|
||||||
delta,
|
|
||||||
searchPostLocal,
|
|
||||||
longDelay,
|
|
||||||
editCommunity,
|
|
||||||
unfollows,
|
|
||||||
getMyUser,
|
|
||||||
userBlockInstance,
|
|
||||||
} from "./shared";
|
|
||||||
import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams";
|
|
||||||
import { EditCommunity, GetPosts } from "lemmy-js-client";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
function assertCommunityFederation(
|
|
||||||
communityOne?: CommunityView,
|
|
||||||
communityTwo?: CommunityView,
|
|
||||||
) {
|
|
||||||
expect(communityOne?.community.actor_id).toBe(
|
|
||||||
communityTwo?.community.actor_id,
|
|
||||||
);
|
|
||||||
expect(communityOne?.community.name).toBe(communityTwo?.community.name);
|
|
||||||
expect(communityOne?.community.title).toBe(communityTwo?.community.title);
|
|
||||||
expect(communityOne?.community.description).toBe(
|
|
||||||
communityTwo?.community.description,
|
|
||||||
);
|
|
||||||
expect(communityOne?.community.icon).toBe(communityTwo?.community.icon);
|
|
||||||
expect(communityOne?.community.banner).toBe(communityTwo?.community.banner);
|
|
||||||
expect(communityOne?.community.published).toBe(
|
|
||||||
communityTwo?.community.published,
|
|
||||||
);
|
|
||||||
expect(communityOne?.community.nsfw).toBe(communityTwo?.community.nsfw);
|
|
||||||
expect(communityOne?.community.removed).toBe(communityTwo?.community.removed);
|
|
||||||
expect(communityOne?.community.deleted).toBe(communityTwo?.community.deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Create community", async () => {
|
|
||||||
let communityRes = await createCommunity(alpha);
|
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
|
||||||
|
|
||||||
// A dupe check
|
|
||||||
let prevName = communityRes.community_view.community.name;
|
|
||||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
|
||||||
Error("community_already_exists"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cache the community on beta, make sure it has the other fields
|
|
||||||
let searchShort = `!${prevName}@lemmy-alpha:8541`;
|
|
||||||
let betaCommunity = (await resolveCommunity(beta, searchShort)).community;
|
|
||||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Delete community", async () => {
|
|
||||||
let communityRes = await createCommunity(beta);
|
|
||||||
|
|
||||||
// Cache the community on Alpha
|
|
||||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
|
||||||
if (!alphaCommunity) {
|
|
||||||
throw "Missing alpha community";
|
|
||||||
}
|
|
||||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
|
||||||
|
|
||||||
// Follow the community from alpha
|
|
||||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
|
||||||
|
|
||||||
// Make sure the follow response went through
|
|
||||||
expect(follow.community_view.community.local).toBe(false);
|
|
||||||
|
|
||||||
let deleteCommunityRes = await deleteCommunity(
|
|
||||||
beta,
|
|
||||||
true,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
|
|
||||||
expect(deleteCommunityRes.community_view.community.title).toBe(
|
|
||||||
communityRes.community_view.community.title,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure it got deleted on A
|
|
||||||
let communityOnAlphaDeleted = await waitUntil(
|
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
|
||||||
g => g.community_view.community.deleted,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true);
|
|
||||||
|
|
||||||
// Undelete
|
|
||||||
let undeleteCommunityRes = await deleteCommunity(
|
|
||||||
beta,
|
|
||||||
false,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(undeleteCommunityRes.community_view.community.deleted).toBe(false);
|
|
||||||
|
|
||||||
// Make sure it got undeleted on A
|
|
||||||
let communityOnAlphaUnDeleted = await waitUntil(
|
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
|
||||||
g => !g.community_view.community.deleted,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Remove community", async () => {
|
|
||||||
let communityRes = await createCommunity(beta);
|
|
||||||
|
|
||||||
// Cache the community on Alpha
|
|
||||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
|
||||||
if (!alphaCommunity) {
|
|
||||||
throw "Missing alpha community";
|
|
||||||
}
|
|
||||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
|
||||||
|
|
||||||
// Follow the community from alpha
|
|
||||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
|
||||||
|
|
||||||
// Make sure the follow response went through
|
|
||||||
expect(follow.community_view.community.local).toBe(false);
|
|
||||||
|
|
||||||
let removeCommunityRes = await removeCommunity(
|
|
||||||
beta,
|
|
||||||
true,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(removeCommunityRes.community_view.community.removed).toBe(true);
|
|
||||||
expect(removeCommunityRes.community_view.community.title).toBe(
|
|
||||||
communityRes.community_view.community.title,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make sure it got Removed on A
|
|
||||||
let communityOnAlphaRemoved = await waitUntil(
|
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
|
||||||
g => g.community_view.community.removed,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true);
|
|
||||||
|
|
||||||
// unremove
|
|
||||||
let unremoveCommunityRes = await removeCommunity(
|
|
||||||
beta,
|
|
||||||
false,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(unremoveCommunityRes.community_view.community.removed).toBe(false);
|
|
||||||
|
|
||||||
// Make sure it got undeleted on A
|
|
||||||
let communityOnAlphaUnRemoved = await waitUntil(
|
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
|
||||||
g => !g.community_view.community.removed,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Search for beta community", async () => {
|
|
||||||
let communityRes = await createCommunity(beta);
|
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
|
||||||
|
|
||||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
|
||||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Admin actions in remote community are not federated to origin", async () => {
|
|
||||||
// create a community on alpha
|
|
||||||
let communityRes = (await createCommunity(alpha)).community_view;
|
|
||||||
expect(communityRes.community.name).toBeDefined();
|
|
||||||
|
|
||||||
// gamma follows community and posts in it
|
|
||||||
let gammaCommunity = (
|
|
||||||
await resolveCommunity(gamma, communityRes.community.actor_id)
|
|
||||||
).community;
|
|
||||||
if (!gammaCommunity) {
|
|
||||||
throw "Missing gamma community";
|
|
||||||
}
|
|
||||||
await followCommunity(gamma, true, gammaCommunity.community.id);
|
|
||||||
gammaCommunity = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveCommunity(gamma, communityRes.community.actor_id),
|
|
||||||
g => g.community?.subscribed === "Subscribed",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
if (!gammaCommunity) {
|
|
||||||
throw "Missing gamma community";
|
|
||||||
}
|
|
||||||
expect(gammaCommunity.subscribed).toBe("Subscribed");
|
|
||||||
let gammaPost = (await createPost(gamma, gammaCommunity.community.id))
|
|
||||||
.post_view;
|
|
||||||
expect(gammaPost.post.id).toBeDefined();
|
|
||||||
expect(gammaPost.creator_banned_from_community).toBe(false);
|
|
||||||
|
|
||||||
// admin of beta decides to ban gamma from community
|
|
||||||
let betaCommunity = (
|
|
||||||
await resolveCommunity(beta, communityRes.community.actor_id)
|
|
||||||
).community;
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let bannedUserInfo1 = (await getMyUser(gamma)).local_user_view.person;
|
|
||||||
if (!bannedUserInfo1) {
|
|
||||||
throw "Missing banned user 1";
|
|
||||||
}
|
|
||||||
let bannedUserInfo2 = (await resolvePerson(beta, bannedUserInfo1.actor_id))
|
|
||||||
.person;
|
|
||||||
if (!bannedUserInfo2) {
|
|
||||||
throw "Missing banned user 2";
|
|
||||||
}
|
|
||||||
let banRes = await banPersonFromCommunity(
|
|
||||||
beta,
|
|
||||||
bannedUserInfo2.person.id,
|
|
||||||
betaCommunity.community.id,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(banRes.banned).toBe(true);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// and neither to gamma
|
|
||||||
let gammaPost2 = await getPost(gamma, gammaPost.post.id);
|
|
||||||
expect(gammaPost2.post_view.creator_banned_from_community).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("moderator view", async () => {
|
|
||||||
// register a new user with their own community on alpha and post to it
|
|
||||||
let otherUser = await registerUser(alpha, alphaUrl);
|
|
||||||
|
|
||||||
let otherCommunity = (await createCommunity(otherUser)).community_view;
|
|
||||||
expect(otherCommunity.community.name).toBeDefined();
|
|
||||||
let otherPost = (await createPost(otherUser, otherCommunity.community.id))
|
|
||||||
.post_view;
|
|
||||||
expect(otherPost.post.id).toBeDefined();
|
|
||||||
|
|
||||||
let otherComment = (await createComment(otherUser, otherPost.post.id))
|
|
||||||
.comment_view;
|
|
||||||
expect(otherComment.comment.id).toBeDefined();
|
|
||||||
|
|
||||||
// create a community and post on alpha
|
|
||||||
let alphaCommunity = (await createCommunity(alpha)).community_view;
|
|
||||||
expect(alphaCommunity.community.name).toBeDefined();
|
|
||||||
let alphaPost = (await createPost(alpha, alphaCommunity.community.id))
|
|
||||||
.post_view;
|
|
||||||
expect(alphaPost.post.id).toBeDefined();
|
|
||||||
|
|
||||||
let alphaComment = (await createComment(otherUser, alphaPost.post.id))
|
|
||||||
.comment_view;
|
|
||||||
expect(alphaComment.comment.id).toBeDefined();
|
|
||||||
|
|
||||||
// other user also posts on alpha's community
|
|
||||||
let otherAlphaPost = (
|
|
||||||
await createPost(otherUser, alphaCommunity.community.id)
|
|
||||||
).post_view;
|
|
||||||
expect(otherAlphaPost.post.id).toBeDefined();
|
|
||||||
|
|
||||||
let otherAlphaComment = (
|
|
||||||
await createComment(otherUser, otherAlphaPost.post.id)
|
|
||||||
).comment_view;
|
|
||||||
expect(otherAlphaComment.comment.id).toBeDefined();
|
|
||||||
|
|
||||||
// alpha lists posts and comments on home page, should contain all posts that were made
|
|
||||||
let posts = (await getPosts(alpha, "All")).posts;
|
|
||||||
expect(posts).toBeDefined();
|
|
||||||
let postIds = posts.map(post => post.post.id);
|
|
||||||
|
|
||||||
let comments = (await getComments(alpha, undefined, "All")).comments;
|
|
||||||
expect(comments).toBeDefined();
|
|
||||||
let commentIds = comments.map(comment => comment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).toContain(otherPost.post.id);
|
|
||||||
expect(commentIds).toContain(otherComment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).toContain(alphaPost.post.id);
|
|
||||||
expect(commentIds).toContain(alphaComment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).toContain(otherAlphaPost.post.id);
|
|
||||||
expect(commentIds).toContain(otherAlphaComment.comment.id);
|
|
||||||
|
|
||||||
// in moderator view, alpha should not see otherPost, wich was posted on a community alpha doesn't moderate
|
|
||||||
posts = (await getPosts(alpha, "ModeratorView")).posts;
|
|
||||||
expect(posts).toBeDefined();
|
|
||||||
postIds = posts.map(post => post.post.id);
|
|
||||||
|
|
||||||
comments = (await getComments(alpha, undefined, "ModeratorView")).comments;
|
|
||||||
expect(comments).toBeDefined();
|
|
||||||
commentIds = comments.map(comment => comment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).not.toContain(otherPost.post.id);
|
|
||||||
expect(commentIds).not.toContain(otherComment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).toContain(alphaPost.post.id);
|
|
||||||
expect(commentIds).toContain(alphaComment.comment.id);
|
|
||||||
|
|
||||||
expect(postIds).toContain(otherAlphaPost.post.id);
|
|
||||||
expect(commentIds).toContain(otherAlphaComment.comment.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Get community for different casing on domain", async () => {
|
|
||||||
let communityRes = await createCommunity(alpha);
|
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
|
||||||
|
|
||||||
// A dupe check
|
|
||||||
let prevName = communityRes.community_view.community.name;
|
|
||||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
|
||||||
Error("community_already_exists"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cache the community on beta, make sure it has the other fields
|
|
||||||
let communityName = `${communityRes.community_view.community.name}@LEMMY-ALPHA:8541`;
|
|
||||||
let betaCommunity = (await getCommunityByName(beta, communityName))
|
|
||||||
.community_view;
|
|
||||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User blocks instance, communities are hidden", async () => {
|
|
||||||
// create community and post on beta
|
|
||||||
let communityRes = await createCommunity(beta);
|
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
|
||||||
let postRes = await createPost(
|
|
||||||
beta,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(postRes.post_view.post.id).toBeDefined();
|
|
||||||
|
|
||||||
// fetch post to alpha
|
|
||||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post!;
|
|
||||||
expect(alphaPost.post).toBeDefined();
|
|
||||||
|
|
||||||
// post should be included in listing
|
|
||||||
let listing = await getPosts(alpha, "All");
|
|
||||||
let listing_ids = listing.posts.map(p => p.post.ap_id);
|
|
||||||
expect(listing_ids).toContain(postRes.post_view.post.ap_id);
|
|
||||||
|
|
||||||
// block the beta instance
|
|
||||||
await userBlockInstance(alpha, alphaPost.community.instance_id, true);
|
|
||||||
|
|
||||||
// after blocking, post should not be in listing
|
|
||||||
let listing2 = await getPosts(alpha, "All");
|
|
||||||
let listing_ids2 = listing2.posts.map(p => p.post.ap_id);
|
|
||||||
expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1);
|
|
||||||
|
|
||||||
// unblock instance again
|
|
||||||
await userBlockInstance(alpha, alphaPost.community.instance_id, false);
|
|
||||||
|
|
||||||
// post should be included in listing
|
|
||||||
let listing3 = await getPosts(alpha, "All");
|
|
||||||
let listing_ids3 = listing3.posts.map(p => p.post.ap_id);
|
|
||||||
expect(listing_ids3).toContain(postRes.post_view.post.ap_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Community follower count is federated", async () => {
|
|
||||||
// Follow the beta community from 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
await followCommunity(alpha, true, resolved.community.community.id);
|
|
||||||
let followed = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveCommunity(alpha, communityActorId),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
|
|
||||||
// Make sure there is 1 subscriber
|
|
||||||
expect(followed?.counts.subscribers).toBe(1);
|
|
||||||
|
|
||||||
// Follow the community from gamma
|
|
||||||
resolved = await resolveCommunity(gamma, communityActorId);
|
|
||||||
if (!resolved.community) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
await followCommunity(gamma, true, resolved.community.community.id);
|
|
||||||
followed = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveCommunity(gamma, communityActorId),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
|
|
||||||
// Make sure there are 2 subscribers
|
|
||||||
expect(followed?.counts?.subscribers).toBe(2);
|
|
||||||
|
|
||||||
// Follow the community from delta
|
|
||||||
resolved = await resolveCommunity(delta, communityActorId);
|
|
||||||
if (!resolved.community) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
await followCommunity(delta, true, resolved.community.community.id);
|
|
||||||
followed = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveCommunity(delta, communityActorId),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
|
|
||||||
// Make sure there are 3 subscribers
|
|
||||||
expect(followed?.counts?.subscribers).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Dont receive community activities after unsubscribe", async () => {
|
|
||||||
let communityRes = await createCommunity(alpha);
|
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
|
||||||
expect(communityRes.community_view.counts.subscribers).toBe(1);
|
|
||||||
|
|
||||||
let betaCommunity = (
|
|
||||||
await resolveCommunity(beta, communityRes.community_view.community.actor_id)
|
|
||||||
).community;
|
|
||||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
|
||||||
|
|
||||||
// follow alpha community from beta
|
|
||||||
await followCommunity(beta, true, betaCommunity!.community.id);
|
|
||||||
|
|
||||||
// ensure that follower count was updated
|
|
||||||
let communityRes1 = await getCommunity(
|
|
||||||
alpha,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(communityRes1.community_view.counts.subscribers).toBe(2);
|
|
||||||
|
|
||||||
// temporarily block alpha, so that it doesn't know about unfollow
|
|
||||||
var allow_instance_params: AdminAllowInstanceParams = {
|
|
||||||
instance: "lemmy-alpha",
|
|
||||||
allow: false,
|
|
||||||
reason: undefined,
|
|
||||||
};
|
|
||||||
await beta.adminAllowInstance(allow_instance_params);
|
|
||||||
await longDelay();
|
|
||||||
|
|
||||||
// unfollow
|
|
||||||
await followCommunity(beta, false, betaCommunity!.community.id);
|
|
||||||
|
|
||||||
// ensure that alpha still sees beta as follower
|
|
||||||
let communityRes2 = await getCommunity(
|
|
||||||
alpha,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(communityRes2.community_view.counts.subscribers).toBe(2);
|
|
||||||
|
|
||||||
// unblock alpha
|
|
||||||
allow_instance_params.allow = true;
|
|
||||||
await beta.adminAllowInstance(allow_instance_params);
|
|
||||||
await longDelay();
|
|
||||||
|
|
||||||
// create a post, it shouldnt reach beta
|
|
||||||
let postRes = await createPost(
|
|
||||||
alpha,
|
|
||||||
communityRes.community_view.community.id,
|
|
||||||
);
|
|
||||||
expect(postRes.post_view.post.id).toBeDefined();
|
|
||||||
// await longDelay();
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(alphaCommunity.community_view.community.description).toBe(
|
|
||||||
"Example description",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Community name with non-ascii chars", async () => {
|
|
||||||
const name = "това_ме_ядосва" + Math.random().toString().slice(2, 6);
|
|
||||||
let communityRes = await createCommunity(alpha, name);
|
|
||||||
|
|
||||||
let betaCommunity1 = await resolveCommunity(
|
|
||||||
beta,
|
|
||||||
communityRes.community_view.community.actor_id,
|
|
||||||
);
|
|
||||||
expect(betaCommunity1.community!.community.name).toBe(name);
|
|
||||||
|
|
||||||
let alphaCommunity2 = await getCommunityByName(alpha, name);
|
|
||||||
expect(alphaCommunity2.community_view.community.name).toBe(name);
|
|
||||||
|
|
||||||
let fediName = `${communityRes.community_view.community.name}@LEMMY-ALPHA:8541`;
|
|
||||||
let betaCommunity2 = await getCommunityByName(beta, fediName);
|
|
||||||
expect(betaCommunity2.community_view.community.name).toBe(name);
|
|
||||||
|
|
||||||
let postRes = await createPost(beta, betaCommunity1.community!.community.id);
|
|
||||||
|
|
||||||
let form: GetPosts = {
|
|
||||||
community_name: fediName,
|
|
||||||
};
|
|
||||||
let posts = await beta.getPosts(form);
|
|
||||||
expect(posts.posts[0].post.name).toBe(postRes.post_view.post.name);
|
|
||||||
});
|
|
123
api_tests/src/follow.spec.ts
vendored
123
api_tests/src/follow.spec.ts
vendored
|
@ -1,123 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
setupLogins,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
followCommunity,
|
|
||||||
waitUntil,
|
|
||||||
beta,
|
|
||||||
betaUrl,
|
|
||||||
registerUser,
|
|
||||||
unfollows,
|
|
||||||
delay,
|
|
||||||
getMyUser,
|
|
||||||
} from "./shared";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
// 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";
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
|
|
||||||
// Make sure the follow response went through
|
|
||||||
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 my_user = await getMyUser(alpha);
|
|
||||||
let remoteCommunityId = my_user?.follows.find(
|
|
||||||
c =>
|
|
||||||
c.community.local == false &&
|
|
||||||
c.community.id === betaCommunityInitial.community.id,
|
|
||||||
)?.community.id;
|
|
||||||
expect(remoteCommunityId).toBeDefined();
|
|
||||||
|
|
||||||
if (!remoteCommunityId) {
|
|
||||||
throw "Missing remote community id";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test an unfollow
|
|
||||||
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
|
|
||||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
|
||||||
|
|
||||||
// Make sure you are unsubbed locally
|
|
||||||
let siteUnfollowCheck = await getMyUser(alpha);
|
|
||||||
expect(
|
|
||||||
siteUnfollowCheck.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);
|
|
||||||
});
|
|
367
api_tests/src/image.spec.ts
vendored
367
api_tests/src/image.spec.ts
vendored
|
@ -1,367 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import {
|
|
||||||
UploadImage,
|
|
||||||
PurgePerson,
|
|
||||||
PurgePost,
|
|
||||||
DeleteImageParams,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
alphaImage,
|
|
||||||
alphaUrl,
|
|
||||||
beta,
|
|
||||||
betaUrl,
|
|
||||||
createCommunity,
|
|
||||||
createPost,
|
|
||||||
deleteAllImages,
|
|
||||||
epsilon,
|
|
||||||
followCommunity,
|
|
||||||
gamma,
|
|
||||||
imageFetchLimit,
|
|
||||||
registerUser,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
resolveCommunity,
|
|
||||||
resolvePost,
|
|
||||||
setupLogins,
|
|
||||||
waitForPost,
|
|
||||||
unfollows,
|
|
||||||
getPost,
|
|
||||||
waitUntil,
|
|
||||||
createPostWithThumbnail,
|
|
||||||
sampleImage,
|
|
||||||
sampleSite,
|
|
||||||
getMyUser,
|
|
||||||
} from "./shared";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await Promise.all([unfollows(), deleteAllImages(alpha)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Upload image and delete it", async () => {
|
|
||||||
const health = await alpha.imageHealth();
|
|
||||||
expect(health.success).toBeTruthy();
|
|
||||||
|
|
||||||
// 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.image_url).toBeDefined();
|
|
||||||
expect(upload.filename).toBeDefined();
|
|
||||||
expect(upload.delete_token).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const response = await fetch(upload.image_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);
|
|
||||||
|
|
||||||
// Make sure the uploader is correct
|
|
||||||
expect(listMediaRes.images[0].person.actor_id).toBe(
|
|
||||||
`http://lemmy-alpha:8541/u/lemmy_alpha`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete image
|
|
||||||
const delete_form: DeleteImageParams = {
|
|
||||||
token: upload.delete_token,
|
|
||||||
filename: upload.filename,
|
|
||||||
};
|
|
||||||
const delete_ = await alphaImage.deleteImage(delete_form);
|
|
||||||
expect(delete_.success).toBe(true);
|
|
||||||
|
|
||||||
// ensure that image is deleted
|
|
||||||
const response2 = await fetch(upload.image_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.filename).toBeDefined();
|
|
||||||
expect(upload.delete_token).toBeDefined();
|
|
||||||
expect(upload.image_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const response = await fetch(upload.image_url ?? "");
|
|
||||||
const content = await response.text();
|
|
||||||
expect(content.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// purge user
|
|
||||||
let my_user = await getMyUser(user);
|
|
||||||
const purgeForm: PurgePerson = {
|
|
||||||
person_id: 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.image_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.filename).toBeDefined();
|
|
||||||
expect(upload.delete_token).toBeDefined();
|
|
||||||
expect(upload.image_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const response = await fetch(upload.image_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.image_url,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post.url).toBe(upload.image_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.image_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/v4/image/proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v4/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/v4/image/proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
epsilonPost.body?.startsWith(
|
|
||||||
"![](http://lemmy-epsilon:8581/api/v4/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/v4/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/v4/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.image_url,
|
|
||||||
`![](${sampleImage})`,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post).toBeDefined();
|
|
||||||
|
|
||||||
// remote image doesn't get proxied after upload
|
|
||||||
expect(
|
|
||||||
post.post_view.post.url?.startsWith("http://lemmy-beta:8551/api/v4/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://lemmy-beta:8551/api/v4/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.image_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.image_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.image_url!,
|
|
||||||
upload2.image_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.image_url);
|
|
||||||
// Make sure the custom thumbnail is ignored
|
|
||||||
expect(post.post_view.post.thumbnail_url == upload2.image_url).toBe(false);
|
|
||||||
});
|
|
835
api_tests/src/post.spec.ts
vendored
835
api_tests/src/post.spec.ts
vendored
|
@ -1,835 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
gamma,
|
|
||||||
delta,
|
|
||||||
epsilon,
|
|
||||||
setupLogins,
|
|
||||||
createPost,
|
|
||||||
editPost,
|
|
||||||
featurePost,
|
|
||||||
lockPost,
|
|
||||||
resolvePost,
|
|
||||||
likePost,
|
|
||||||
followBeta,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
createComment,
|
|
||||||
deletePost,
|
|
||||||
delay,
|
|
||||||
removePost,
|
|
||||||
getPost,
|
|
||||||
unfollowRemotes,
|
|
||||||
resolvePerson,
|
|
||||||
banPersonFromSite,
|
|
||||||
followCommunity,
|
|
||||||
banPersonFromCommunity,
|
|
||||||
reportPost,
|
|
||||||
randomString,
|
|
||||||
registerUser,
|
|
||||||
unfollows,
|
|
||||||
resolveCommunity,
|
|
||||||
waitUntil,
|
|
||||||
waitForPost,
|
|
||||||
alphaUrl,
|
|
||||||
loginUser,
|
|
||||||
createCommunity,
|
|
||||||
listReports,
|
|
||||||
getMyUser,
|
|
||||||
} from "./shared";
|
|
||||||
import { PostView } from "lemmy-js-client/dist/types/PostView";
|
|
||||||
import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams";
|
|
||||||
import {
|
|
||||||
EditSite,
|
|
||||||
PostReport,
|
|
||||||
PostReportView,
|
|
||||||
ReportCombinedView,
|
|
||||||
ResolveObject,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
|
|
||||||
let betaCommunity: CommunityView | undefined;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await setupLogins();
|
|
||||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
|
||||||
expect(betaCommunity).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
// TODO url clears arent working
|
|
||||||
// expect(postOne?.post.url).toBe(postTwo?.post.url);
|
|
||||||
expect(postOne?.post.nsfw).toBe(postTwo?.post.nsfw);
|
|
||||||
expect(postOne?.post.embed_title).toBe(postTwo?.post.embed_title);
|
|
||||||
expect(postOne?.post.embed_description).toBe(postTwo?.post.embed_description);
|
|
||||||
expect(postOne?.post.embed_video_url).toBe(postTwo?.post.embed_video_url);
|
|
||||||
expect(postOne?.post.published).toBe(postTwo?.post.published);
|
|
||||||
expect(postOne?.community.actor_id).toBe(postTwo?.community.actor_id);
|
|
||||||
expect(postOne?.post.locked).toBe(postTwo?.post.locked);
|
|
||||||
expect(postOne?.post.removed).toBe(postTwo?.post.removed);
|
|
||||||
expect(postOne?.post.deleted).toBe(postTwo?.post.deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Create a post", async () => {
|
|
||||||
// Block alpha
|
|
||||||
var block_instance_params: AdminBlockInstanceParams = {
|
|
||||||
instance: "lemmy-alpha",
|
|
||||||
block: true,
|
|
||||||
};
|
|
||||||
await epsilon.adminBlockInstance(block_instance_params);
|
|
||||||
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
expect(postRes.post_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure that post is liked on beta
|
|
||||||
const betaPost = await waitForPost(
|
|
||||||
beta,
|
|
||||||
postRes.post_view.post,
|
|
||||||
res => res?.counts.score === 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(betaPost).toBeDefined();
|
|
||||||
expect(betaPost?.community.local).toBe(true);
|
|
||||||
expect(betaPost?.creator.local).toBe(false);
|
|
||||||
expect(betaPost?.counts.score).toBe(1);
|
|
||||||
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.toStrictEqual(Error("not_found"));
|
|
||||||
|
|
||||||
// Epsilon has alpha blocked, it should not see the alpha post
|
|
||||||
await expect(
|
|
||||||
resolvePost(epsilon, postRes.post_view.post),
|
|
||||||
).rejects.toStrictEqual(Error("not_found"));
|
|
||||||
|
|
||||||
// remove blocked instance
|
|
||||||
block_instance_params.block = false;
|
|
||||||
await epsilon.adminBlockInstance(block_instance_params);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create a post in a non-existent community", async () => {
|
|
||||||
await expect(createPost(alpha, -2)).rejects.toStrictEqual(Error("not_found"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Unlike a post", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
let unlike = await likePost(alpha, 0, postRes.post_view.post);
|
|
||||||
expect(unlike.post_view.counts.score).toBe(0);
|
|
||||||
|
|
||||||
// Try to unlike it again, make sure it stays at 0
|
|
||||||
let unlike2 = await likePost(alpha, 0, postRes.post_view.post);
|
|
||||||
expect(unlike2.post_view.counts.score).toBe(0);
|
|
||||||
|
|
||||||
// Make sure that post is unliked on beta
|
|
||||||
const betaPost = await waitForPost(
|
|
||||||
beta,
|
|
||||||
postRes.post_view.post,
|
|
||||||
post => post?.counts.score === 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(betaPost).toBeDefined();
|
|
||||||
expect(betaPost?.community.local).toBe(true);
|
|
||||||
expect(betaPost?.creator.local).toBe(false);
|
|
||||||
expect(betaPost?.counts.score).toBe(0);
|
|
||||||
await assertPostFederation(betaPost, postRes.post_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Update a post", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
await waitForPost(beta, postRes.post_view.post);
|
|
||||||
|
|
||||||
let updatedName = "A jest test federated post, updated";
|
|
||||||
let updatedPost = await editPost(alpha, postRes.post_view.post);
|
|
||||||
expect(updatedPost.post_view.post.name).toBe(updatedName);
|
|
||||||
expect(updatedPost.post_view.community.local).toBe(false);
|
|
||||||
expect(updatedPost.post_view.creator.local).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that post is updated on beta
|
|
||||||
let betaPost = await waitForPost(beta, updatedPost.post_view.post);
|
|
||||||
expect(betaPost.community.local).toBe(true);
|
|
||||||
expect(betaPost.creator.local).toBe(false);
|
|
||||||
expect(betaPost.post.name).toBe(updatedName);
|
|
||||||
await assertPostFederation(betaPost, updatedPost.post_view);
|
|
||||||
|
|
||||||
// Make sure lemmy beta cannot update the post
|
|
||||||
await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual(
|
|
||||||
Error("no_post_edit_allowed"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Sticky a post", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
|
|
||||||
let betaPost1 = await waitForPost(beta, postRes.post_view.post);
|
|
||||||
if (!betaPost1) {
|
|
||||||
throw "Missing beta post1";
|
|
||||||
}
|
|
||||||
let stickiedPostRes = await featurePost(beta, true, betaPost1.post);
|
|
||||||
expect(stickiedPostRes.post_view.post.featured_community).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that post is stickied on beta
|
|
||||||
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
|
|
||||||
expect(betaPost?.community.local).toBe(true);
|
|
||||||
expect(betaPost?.creator.local).toBe(false);
|
|
||||||
expect(betaPost?.post.featured_community).toBe(true);
|
|
||||||
|
|
||||||
// Unsticky a post
|
|
||||||
let unstickiedPost = await featurePost(beta, false, betaPost1.post);
|
|
||||||
expect(unstickiedPost.post_view.post.featured_community).toBe(false);
|
|
||||||
|
|
||||||
// Make sure that post is unstickied on beta
|
|
||||||
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
|
|
||||||
expect(betaPost2?.community.local).toBe(true);
|
|
||||||
expect(betaPost2?.creator.local).toBe(false);
|
|
||||||
expect(betaPost2?.post.featured_community).toBe(false);
|
|
||||||
|
|
||||||
// Make sure that gamma cannot sticky the post on beta
|
|
||||||
let gammaPost = (await resolvePost(gamma, postRes.post_view.post)).post;
|
|
||||||
if (!gammaPost) {
|
|
||||||
throw "Missing gamma 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(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";
|
|
||||||
}
|
|
||||||
await followCommunity(alpha, true, betaCommunity.community.id);
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveBetaCommunity(alpha),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
);
|
|
||||||
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
let betaPost1 = await waitForPost(beta, postRes.post_view.post);
|
|
||||||
// Lock the post
|
|
||||||
let lockedPostRes = await lockPost(beta, true, betaPost1.post);
|
|
||||||
expect(lockedPostRes.post_view.post.locked).toBe(true);
|
|
||||||
|
|
||||||
// Make sure that post is locked on alpha
|
|
||||||
let alphaPost1 = await waitForPost(
|
|
||||||
alpha,
|
|
||||||
postRes.post_view.post,
|
|
||||||
post => !!post && post.post.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);
|
|
||||||
expect(unlockedPost.post_view.post.locked).toBe(false);
|
|
||||||
|
|
||||||
// Make sure that post is unlocked on alpha
|
|
||||||
let alphaPost2 = await waitForPost(
|
|
||||||
alpha,
|
|
||||||
postRes.post_view.post,
|
|
||||||
post => !!post && !post.post.locked,
|
|
||||||
);
|
|
||||||
expect(alphaPost2.community.local).toBe(false);
|
|
||||||
expect(alphaPost2.creator.local).toBe(true);
|
|
||||||
expect(alphaPost2.post.locked).toBe(false);
|
|
||||||
|
|
||||||
// Try to create a new comment, on alpha
|
|
||||||
let commentAlpha = await createComment(user, alphaPost1.post.id);
|
|
||||||
expect(commentAlpha).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Delete a post", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
expect(postRes.post_view.post).toBeDefined();
|
|
||||||
await waitForPost(beta, postRes.post_view.post);
|
|
||||||
|
|
||||||
let deletedPost = await deletePost(alpha, true, postRes.post_view.post);
|
|
||||||
expect(deletedPost.post_view.post.deleted).toBe(true);
|
|
||||||
expect(deletedPost.post_view.post.name).toBe(postRes.post_view.post.name);
|
|
||||||
|
|
||||||
// Make sure lemmy beta sees post is deleted
|
|
||||||
// This will be undefined because of the tombstone
|
|
||||||
await waitForPost(beta, postRes.post_view.post, p => !p || p.post.deleted);
|
|
||||||
|
|
||||||
// Undelete
|
|
||||||
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post);
|
|
||||||
|
|
||||||
// Make sure lemmy beta sees post is undeleted
|
|
||||||
let betaPost2 = await waitForPost(
|
|
||||||
beta,
|
|
||||||
postRes.post_view.post,
|
|
||||||
p => !!p && !p.post.deleted,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!betaPost2) {
|
|
||||||
throw "Missing beta post 2";
|
|
||||||
}
|
|
||||||
expect(betaPost2.post.deleted).toBe(false);
|
|
||||||
await assertPostFederation(betaPost2, undeletedPost.post_view);
|
|
||||||
|
|
||||||
// Make sure lemmy beta cannot delete the post
|
|
||||||
await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual(
|
|
||||||
Error("no_post_edit_allowed"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Remove a post from admin and community on different instance", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
let gammaCommunity = (
|
|
||||||
await resolveCommunity(gamma, betaCommunity.community.actor_id)
|
|
||||||
).community?.community;
|
|
||||||
if (!gammaCommunity) {
|
|
||||||
throw "Missing gamma community";
|
|
||||||
}
|
|
||||||
let postRes = await createPost(gamma, gammaCommunity.id);
|
|
||||||
|
|
||||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
|
|
||||||
if (!alphaPost) {
|
|
||||||
throw "Missing alpha post";
|
|
||||||
}
|
|
||||||
let removedPost = await removePost(alpha, true, alphaPost.post);
|
|
||||||
expect(removedPost.post_view.post.removed).toBe(true);
|
|
||||||
expect(removedPost.post_view.post.name).toBe(postRes.post_view.post.name);
|
|
||||||
|
|
||||||
// Make sure lemmy beta sees post is NOT removed
|
|
||||||
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
|
|
||||||
if (!betaPost) {
|
|
||||||
throw "Missing beta post";
|
|
||||||
}
|
|
||||||
expect(betaPost.post.removed).toBe(false);
|
|
||||||
|
|
||||||
// Undelete
|
|
||||||
let undeletedPost = await removePost(alpha, false, alphaPost.post);
|
|
||||||
expect(undeletedPost.post_view.post.removed).toBe(false);
|
|
||||||
|
|
||||||
// Make sure lemmy beta sees post is undeleted
|
|
||||||
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
|
|
||||||
expect(betaPost2?.post.removed).toBe(false);
|
|
||||||
await assertPostFederation(betaPost2!, undeletedPost.post_view);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Remove a post from admin and community on same instance", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
await followBeta(alpha);
|
|
||||||
let gammaCommunity = await resolveCommunity(
|
|
||||||
gamma,
|
|
||||||
betaCommunity.community.actor_id,
|
|
||||||
);
|
|
||||||
let postRes = await createPost(gamma, gammaCommunity.community!.community.id);
|
|
||||||
expect(postRes.post_view.post).toBeDefined();
|
|
||||||
// Get the id for beta
|
|
||||||
let betaPost = await waitForPost(beta, postRes.post_view.post);
|
|
||||||
expect(betaPost).toBeDefined();
|
|
||||||
|
|
||||||
let alphaPost0 = await waitForPost(alpha, postRes.post_view.post);
|
|
||||||
expect(alphaPost0).toBeDefined();
|
|
||||||
|
|
||||||
// The beta admin removes it (the community lives on beta)
|
|
||||||
let removePostRes = await removePost(beta, true, betaPost.post);
|
|
||||||
expect(removePostRes.post_view.post.removed).toBe(true);
|
|
||||||
|
|
||||||
// Make sure lemmy alpha sees post is removed
|
|
||||||
let alphaPost = await waitUntil(
|
|
||||||
() => getPost(alpha, alphaPost0.post.id),
|
|
||||||
p => p?.post_view.post.removed ?? false,
|
|
||||||
);
|
|
||||||
expect(alphaPost?.post_view.post.removed).toBe(true);
|
|
||||||
await assertPostFederation(
|
|
||||||
alphaPost.post_view,
|
|
||||||
removePostRes.post_view,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Undelete
|
|
||||||
let undeletedPost = await removePost(beta, false, betaPost.post);
|
|
||||||
expect(undeletedPost.post_view.post.removed).toBe(false);
|
|
||||||
|
|
||||||
// Make sure lemmy alpha sees post is undeleted
|
|
||||||
let alphaPost2 = await waitForPost(
|
|
||||||
alpha,
|
|
||||||
postRes.post_view.post,
|
|
||||||
p => !!p && !p.post.removed,
|
|
||||||
);
|
|
||||||
expect(alphaPost2.post.removed).toBe(false);
|
|
||||||
await assertPostFederation(alphaPost2, undeletedPost.post_view);
|
|
||||||
await unfollowRemotes(alpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Search for a post", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
await unfollowRemotes(alpha);
|
|
||||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
expect(postRes.post_view.post).toBeDefined();
|
|
||||||
|
|
||||||
let betaPost = await waitForPost(beta, postRes.post_view.post);
|
|
||||||
expect(betaPost?.post.name).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Enforce site ban federation for local user", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a test user
|
|
||||||
let alphaUserHttp = await registerUser(alpha, alphaUrl);
|
|
||||||
let alphaUserPerson = (await getMyUser(alphaUserHttp)).local_user_view.person;
|
|
||||||
let alphaUserActorId = alphaUserPerson?.actor_id;
|
|
||||||
if (!alphaUserActorId) {
|
|
||||||
throw "Missing alpha user actor id";
|
|
||||||
}
|
|
||||||
expect(alphaUserActorId).toBeDefined();
|
|
||||||
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(alphaUserHttp, betaCommunity.community.id);
|
|
||||||
let searchBeta1 = await waitForPost(beta, postRes1.post_view.post);
|
|
||||||
|
|
||||||
// ban alpha from its own instance
|
|
||||||
let banAlpha = await banPersonFromSite(
|
|
||||||
alpha,
|
|
||||||
alphaPerson.person.id,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(banAlpha.banned).toBe(true);
|
|
||||||
|
|
||||||
// alpha ban should be federated to beta
|
|
||||||
let alphaUserOnBeta1 = await waitUntil(
|
|
||||||
() => resolvePerson(beta, alphaUserActorId!),
|
|
||||||
res => res.person?.person.banned ?? false,
|
|
||||||
);
|
|
||||||
expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Unban alpha
|
|
||||||
let unBanAlpha = await banPersonFromSite(
|
|
||||||
alpha,
|
|
||||||
alphaPerson.person.id,
|
|
||||||
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);
|
|
||||||
alphaUserHttp.setHeaders({
|
|
||||||
Authorization: "Bearer " + newAlphaUserJwt.jwt,
|
|
||||||
});
|
|
||||||
// alpha makes new post in beta community, it federates
|
|
||||||
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id);
|
|
||||||
await waitForPost(beta, postRes2.post_view.post);
|
|
||||||
|
|
||||||
await unfollowRemotes(alpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Enforce site ban federation for federated user", async () => {
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a test user
|
|
||||||
let alphaUserHttp = await registerUser(alpha, alphaUrl);
|
|
||||||
let alphaUserPerson = (await getMyUser(alphaUserHttp)).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 getMyUser(alphaUserHttp)).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) {
|
|
||||||
throw "Missing alpha person";
|
|
||||||
}
|
|
||||||
expect(alphaPerson).toBeDefined();
|
|
||||||
|
|
||||||
// make a post in beta, it goes through
|
|
||||||
let postRes1 = await createPost(alpha, betaCommunity.community.id);
|
|
||||||
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,
|
|
||||||
searchBeta1.community.id,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
expect(banAlpha.banned).toBe(true);
|
|
||||||
|
|
||||||
// ensure that the post by alpha got removed
|
|
||||||
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.toStrictEqual(Error("person_is_banned_from_community"));
|
|
||||||
|
|
||||||
// Unban alpha
|
|
||||||
let unBanAlpha = await banPersonFromCommunity(
|
|
||||||
beta,
|
|
||||||
alphaPerson.person.id,
|
|
||||||
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);
|
|
||||||
expect(postRes3.post_view.creator.local).toBe(true);
|
|
||||||
expect(postRes3.post_view.counts.score).toBe(1);
|
|
||||||
|
|
||||||
// Make sure that post makes it to beta community
|
|
||||||
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 () => {
|
|
||||||
// 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
() =>
|
|
||||||
listReports(beta).then(p =>
|
|
||||||
p.reports.find(r => {
|
|
||||||
return checkPostReportName(r, gammaReport);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
res => !!res,
|
|
||||||
))! as PostReportView
|
|
||||||
).post_report;
|
|
||||||
expect(betaReport).toBeDefined();
|
|
||||||
expect(betaReport.resolved).toBe(false);
|
|
||||||
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(
|
|
||||||
() =>
|
|
||||||
listReports(alpha).then(p =>
|
|
||||||
p.reports.find(r => {
|
|
||||||
return checkPostReportName(r, gammaReport);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
res => !!res,
|
|
||||||
))! as PostReportView
|
|
||||||
).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})`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkPostReportName(rcv: ReportCombinedView, report: PostReport) {
|
|
||||||
switch (rcv.type_) {
|
|
||||||
case "Post":
|
|
||||||
return rcv.post_report.original_post_name === report.original_post_name;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
357
api_tests/src/private_community.spec.ts
vendored
357
api_tests/src/private_community.spec.ts
vendored
|
@ -1,357 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import { FollowCommunity, LemmyHttp } from "lemmy-js-client";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
setupLogins,
|
|
||||||
createCommunity,
|
|
||||||
unfollows,
|
|
||||||
registerUser,
|
|
||||||
listCommunityPendingFollows,
|
|
||||||
getCommunity,
|
|
||||||
getCommunityPendingFollowsCount,
|
|
||||||
approveCommunityPendingFollow,
|
|
||||||
randomString,
|
|
||||||
createPost,
|
|
||||||
createComment,
|
|
||||||
beta,
|
|
||||||
resolveCommunity,
|
|
||||||
betaUrl,
|
|
||||||
resolvePost,
|
|
||||||
resolveComment,
|
|
||||||
likeComment,
|
|
||||||
waitUntil,
|
|
||||||
gamma,
|
|
||||||
getPosts,
|
|
||||||
getComments,
|
|
||||||
} from "./shared";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
test("Follow a private community", async () => {
|
|
||||||
// create private community
|
|
||||||
const community = await createCommunity(alpha, randomString(10), "Private");
|
|
||||||
expect(community.community_view.community.visibility).toBe("Private");
|
|
||||||
const alphaCommunityId = community.community_view.community.id;
|
|
||||||
|
|
||||||
// No pending follows yet
|
|
||||||
const pendingFollows0 = await listCommunityPendingFollows(alpha);
|
|
||||||
expect(pendingFollows0.items.length).toBe(0);
|
|
||||||
const pendingFollowsCount0 = await getCommunityPendingFollowsCount(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
);
|
|
||||||
expect(pendingFollowsCount0.count).toBe(0);
|
|
||||||
|
|
||||||
// follow as new user
|
|
||||||
const user = await registerUser(beta, betaUrl);
|
|
||||||
const betaCommunity = (
|
|
||||||
await resolveCommunity(user, community.community_view.community.actor_id)
|
|
||||||
).community;
|
|
||||||
expect(betaCommunity).toBeDefined();
|
|
||||||
expect(betaCommunity?.community.visibility).toBe("Private");
|
|
||||||
const betaCommunityId = betaCommunity!.community.id;
|
|
||||||
const follow_form: FollowCommunity = {
|
|
||||||
community_id: betaCommunityId,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
await user.followCommunity(follow_form);
|
|
||||||
|
|
||||||
// Follow listed as pending
|
|
||||||
const follow1 = await getCommunity(user, betaCommunityId);
|
|
||||||
expect(follow1.community_view.subscribed).toBe("ApprovalRequired");
|
|
||||||
|
|
||||||
// Wait for follow to federate, shown as pending
|
|
||||||
let pendingFollows1 = await waitUntil(
|
|
||||||
() => listCommunityPendingFollows(alpha),
|
|
||||||
f => f.items.length == 1,
|
|
||||||
);
|
|
||||||
expect(pendingFollows1.items[0].is_new_instance).toBe(true);
|
|
||||||
const pendingFollowsCount1 = await getCommunityPendingFollowsCount(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
);
|
|
||||||
expect(pendingFollowsCount1.count).toBe(1);
|
|
||||||
|
|
||||||
// user still sees approval required at this point
|
|
||||||
const betaCommunity2 = await getCommunity(user, betaCommunityId);
|
|
||||||
expect(betaCommunity2.community_view.subscribed).toBe("ApprovalRequired");
|
|
||||||
|
|
||||||
// Approve the follow
|
|
||||||
const approve = await approveCommunityPendingFollow(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
pendingFollows1.items[0].person.id,
|
|
||||||
);
|
|
||||||
expect(approve.success).toBe(true);
|
|
||||||
|
|
||||||
// Follow is confirmed
|
|
||||||
await waitUntil(
|
|
||||||
() => getCommunity(user, betaCommunityId),
|
|
||||||
c => c.community_view.subscribed == "Subscribed",
|
|
||||||
);
|
|
||||||
const pendingFollows2 = await listCommunityPendingFollows(alpha);
|
|
||||||
expect(pendingFollows2.items.length).toBe(0);
|
|
||||||
const pendingFollowsCount2 = await getCommunityPendingFollowsCount(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
);
|
|
||||||
expect(pendingFollowsCount2.count).toBe(0);
|
|
||||||
|
|
||||||
// follow with another user from that instance, is_new_instance should be false now
|
|
||||||
const user2 = await registerUser(beta, betaUrl);
|
|
||||||
await user2.followCommunity(follow_form);
|
|
||||||
let pendingFollows3 = await waitUntil(
|
|
||||||
() => listCommunityPendingFollows(alpha),
|
|
||||||
f => f.items.length == 1,
|
|
||||||
);
|
|
||||||
expect(pendingFollows3.items[0].is_new_instance).toBe(false);
|
|
||||||
|
|
||||||
// cleanup pending follow
|
|
||||||
const approve2 = await approveCommunityPendingFollow(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
pendingFollows3.items[0].person.id,
|
|
||||||
);
|
|
||||||
expect(approve2.success).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Only followers can view and interact with private community content", async () => {
|
|
||||||
// create private community
|
|
||||||
const community = await createCommunity(alpha, randomString(10), "Private");
|
|
||||||
expect(community.community_view.community.visibility).toBe("Private");
|
|
||||||
const alphaCommunityId = community.community_view.community.id;
|
|
||||||
|
|
||||||
// create post and comment
|
|
||||||
const post0 = await createPost(alpha, alphaCommunityId);
|
|
||||||
const post_id = post0.post_view.post.id;
|
|
||||||
expect(post_id).toBeDefined();
|
|
||||||
const comment = await createComment(alpha, post_id);
|
|
||||||
const comment_id = comment.comment_view.comment.id;
|
|
||||||
expect(comment_id).toBeDefined();
|
|
||||||
|
|
||||||
// user is not following the community and cannot view nor create posts
|
|
||||||
const user = await registerUser(beta, betaUrl);
|
|
||||||
const betaCommunity = (
|
|
||||||
await resolveCommunity(user, community.community_view.community.actor_id)
|
|
||||||
).community!.community;
|
|
||||||
await expect(resolvePost(user, post0.post_view.post)).rejects.toStrictEqual(
|
|
||||||
Error("not_found"),
|
|
||||||
);
|
|
||||||
await expect(
|
|
||||||
resolveComment(user, comment.comment_view.comment),
|
|
||||||
).rejects.toStrictEqual(Error("not_found"));
|
|
||||||
await expect(createPost(user, betaCommunity.id)).rejects.toStrictEqual(
|
|
||||||
Error("not_found"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// follow the community and approve
|
|
||||||
const follow_form: FollowCommunity = {
|
|
||||||
community_id: betaCommunity.id,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
await user.followCommunity(follow_form);
|
|
||||||
approveFollower(alpha, alphaCommunityId);
|
|
||||||
|
|
||||||
// now user can fetch posts and comments in community (using signed fetch), and create posts
|
|
||||||
await waitUntil(
|
|
||||||
() => resolvePost(user, post0.post_view.post),
|
|
||||||
p => p?.post?.post.id != undefined,
|
|
||||||
);
|
|
||||||
const resolvedComment = (
|
|
||||||
await resolveComment(user, comment.comment_view.comment)
|
|
||||||
).comment;
|
|
||||||
expect(resolvedComment?.comment.id).toBeDefined();
|
|
||||||
|
|
||||||
const post1 = await createPost(user, betaCommunity.id);
|
|
||||||
expect(post1.post_view).toBeDefined();
|
|
||||||
const like = await likeComment(user, 1, resolvedComment!.comment);
|
|
||||||
expect(like.comment_view.my_vote).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Reject follower", async () => {
|
|
||||||
// create private community
|
|
||||||
const community = await createCommunity(alpha, randomString(10), "Private");
|
|
||||||
expect(community.community_view.community.visibility).toBe("Private");
|
|
||||||
const alphaCommunityId = community.community_view.community.id;
|
|
||||||
|
|
||||||
// user is not following the community and cannot view nor create posts
|
|
||||||
const user = await registerUser(beta, betaUrl);
|
|
||||||
const betaCommunity1 = (
|
|
||||||
await resolveCommunity(user, community.community_view.community.actor_id)
|
|
||||||
).community!.community;
|
|
||||||
|
|
||||||
// follow the community and reject
|
|
||||||
const follow_form: FollowCommunity = {
|
|
||||||
community_id: betaCommunity1.id,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
const follow = await user.followCommunity(follow_form);
|
|
||||||
expect(follow.community_view.subscribed).toBe("ApprovalRequired");
|
|
||||||
|
|
||||||
const pendingFollows1 = await waitUntil(
|
|
||||||
() => listCommunityPendingFollows(alpha),
|
|
||||||
f => f.items.length == 1,
|
|
||||||
);
|
|
||||||
const approve = await approveCommunityPendingFollow(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
pendingFollows1.items[0].person.id,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
expect(approve.success).toBe(true);
|
|
||||||
|
|
||||||
await waitUntil(
|
|
||||||
() => getCommunity(user, betaCommunity1.id),
|
|
||||||
c => c.community_view.subscribed == "NotSubscribed",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Follow a private community and receive activities", async () => {
|
|
||||||
// create private community
|
|
||||||
const community = await createCommunity(alpha, randomString(10), "Private");
|
|
||||||
expect(community.community_view.community.visibility).toBe("Private");
|
|
||||||
const alphaCommunityId = community.community_view.community.id;
|
|
||||||
|
|
||||||
// follow with users from beta and gamma
|
|
||||||
const betaCommunity = (
|
|
||||||
await resolveCommunity(beta, community.community_view.community.actor_id)
|
|
||||||
).community;
|
|
||||||
expect(betaCommunity).toBeDefined();
|
|
||||||
const betaCommunityId = betaCommunity!.community.id;
|
|
||||||
const follow_form_beta: FollowCommunity = {
|
|
||||||
community_id: betaCommunityId,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
await beta.followCommunity(follow_form_beta);
|
|
||||||
await approveFollower(alpha, alphaCommunityId);
|
|
||||||
|
|
||||||
const gammaCommunityId = (
|
|
||||||
await resolveCommunity(gamma, community.community_view.community.actor_id)
|
|
||||||
).community!.community.id;
|
|
||||||
const follow_form_gamma: FollowCommunity = {
|
|
||||||
community_id: gammaCommunityId,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
await gamma.followCommunity(follow_form_gamma);
|
|
||||||
await approveFollower(alpha, alphaCommunityId);
|
|
||||||
|
|
||||||
// Follow is confirmed
|
|
||||||
await waitUntil(
|
|
||||||
() => getCommunity(beta, betaCommunityId),
|
|
||||||
c => c.community_view.subscribed == "Subscribed",
|
|
||||||
);
|
|
||||||
await waitUntil(
|
|
||||||
() => getCommunity(gamma, gammaCommunityId),
|
|
||||||
c => c.community_view.subscribed == "Subscribed",
|
|
||||||
);
|
|
||||||
|
|
||||||
// create a post and comment from gamma
|
|
||||||
const post = await createPost(gamma, gammaCommunityId);
|
|
||||||
const post_id = post.post_view.post.id;
|
|
||||||
expect(post_id).toBeDefined();
|
|
||||||
const comment = await createComment(gamma, post_id);
|
|
||||||
const comment_id = comment.comment_view.comment.id;
|
|
||||||
expect(comment_id).toBeDefined();
|
|
||||||
|
|
||||||
// post and comment were federated to beta
|
|
||||||
let posts = await waitUntil(
|
|
||||||
() => getPosts(beta, "All", betaCommunityId),
|
|
||||||
c => c.posts.length == 1,
|
|
||||||
);
|
|
||||||
expect(posts.posts[0].post.ap_id).toBe(post.post_view.post.ap_id);
|
|
||||||
expect(posts.posts[0].post.name).toBe(post.post_view.post.name);
|
|
||||||
let comments = await waitUntil(
|
|
||||||
() => getComments(beta, posts.posts[0].post.id),
|
|
||||||
c => c.comments.length == 1,
|
|
||||||
);
|
|
||||||
expect(comments.comments[0].comment.ap_id).toBe(
|
|
||||||
comment.comment_view.comment.ap_id,
|
|
||||||
);
|
|
||||||
expect(comments.comments[0].comment.content).toBe(
|
|
||||||
comment.comment_view.comment.content,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fetch remote content in private community", async () => {
|
|
||||||
// create private community
|
|
||||||
const community = await createCommunity(alpha, randomString(10), "Private");
|
|
||||||
expect(community.community_view.community.visibility).toBe("Private");
|
|
||||||
const alphaCommunityId = community.community_view.community.id;
|
|
||||||
|
|
||||||
const betaCommunityId = (
|
|
||||||
await resolveCommunity(beta, community.community_view.community.actor_id)
|
|
||||||
).community!.community.id;
|
|
||||||
const follow_form_beta: FollowCommunity = {
|
|
||||||
community_id: betaCommunityId,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
await beta.followCommunity(follow_form_beta);
|
|
||||||
await approveFollower(alpha, alphaCommunityId);
|
|
||||||
|
|
||||||
// Follow is confirmed
|
|
||||||
await waitUntil(
|
|
||||||
() => getCommunity(beta, betaCommunityId),
|
|
||||||
c => c.community_view.subscribed == "Subscribed",
|
|
||||||
);
|
|
||||||
|
|
||||||
// beta creates post and comment
|
|
||||||
const post = await createPost(beta, betaCommunityId);
|
|
||||||
const post_id = post.post_view.post.id;
|
|
||||||
expect(post_id).toBeDefined();
|
|
||||||
const comment = await createComment(beta, post_id);
|
|
||||||
const comment_id = comment.comment_view.comment.id;
|
|
||||||
expect(comment_id).toBeDefined();
|
|
||||||
|
|
||||||
// Wait for it to federate
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveComment(alpha, comment.comment_view.comment),
|
|
||||||
p => p?.comment?.comment.id != undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
// create gamma user
|
|
||||||
const gammaCommunityId = (
|
|
||||||
await resolveCommunity(gamma, community.community_view.community.actor_id)
|
|
||||||
).community!.community.id;
|
|
||||||
const follow_form: FollowCommunity = {
|
|
||||||
community_id: gammaCommunityId,
|
|
||||||
follow: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// cannot fetch post yet
|
|
||||||
await expect(resolvePost(gamma, post.post_view.post)).rejects.toStrictEqual(
|
|
||||||
Error("not_found"),
|
|
||||||
);
|
|
||||||
// follow community and approve
|
|
||||||
await gamma.followCommunity(follow_form);
|
|
||||||
await approveFollower(alpha, alphaCommunityId);
|
|
||||||
|
|
||||||
// now user can fetch posts and comments in community (using signed fetch), and create posts.
|
|
||||||
// for this to work, beta checks with alpha if gamma is really an approved follower.
|
|
||||||
let resolvedPost = await waitUntil(
|
|
||||||
() => resolvePost(gamma, post.post_view.post),
|
|
||||||
p => p?.post?.post.id != undefined,
|
|
||||||
);
|
|
||||||
expect(resolvedPost.post?.post.ap_id).toBe(post.post_view.post.ap_id);
|
|
||||||
const resolvedComment = await waitUntil(
|
|
||||||
() => resolveComment(gamma, comment.comment_view.comment),
|
|
||||||
p => p?.comment?.comment.id != undefined,
|
|
||||||
);
|
|
||||||
expect(resolvedComment?.comment?.comment.ap_id).toBe(
|
|
||||||
comment.comment_view.comment.ap_id,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function approveFollower(user: LemmyHttp, community_id: number) {
|
|
||||||
let pendingFollows1 = await waitUntil(
|
|
||||||
() => listCommunityPendingFollows(user),
|
|
||||||
f => f.items.length == 1,
|
|
||||||
);
|
|
||||||
const approve = await approveCommunityPendingFollow(
|
|
||||||
alpha,
|
|
||||||
community_id,
|
|
||||||
pendingFollows1.items[0].person.id,
|
|
||||||
);
|
|
||||||
expect(approve.success).toBe(true);
|
|
||||||
}
|
|
149
api_tests/src/private_message.spec.ts
vendored
149
api_tests/src/private_message.spec.ts
vendored
|
@ -1,149 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
setupLogins,
|
|
||||||
followBeta,
|
|
||||||
createPrivateMessage,
|
|
||||||
editPrivateMessage,
|
|
||||||
listPrivateMessages,
|
|
||||||
deletePrivateMessage,
|
|
||||||
waitUntil,
|
|
||||||
reportPrivateMessage,
|
|
||||||
unfollows,
|
|
||||||
} from "./shared";
|
|
||||||
|
|
||||||
let recipient_id: number;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await setupLogins();
|
|
||||||
await followBeta(alpha);
|
|
||||||
recipient_id = 3;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
test("Create a private message", async () => {
|
|
||||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
|
||||||
expect(pmRes.private_message_view.private_message.content).toBeDefined();
|
|
||||||
expect(pmRes.private_message_view.private_message.local).toBe(true);
|
|
||||||
expect(pmRes.private_message_view.creator.local).toBe(true);
|
|
||||||
expect(pmRes.private_message_view.recipient.local).toBe(false);
|
|
||||||
|
|
||||||
let betaPms = await waitUntil(
|
|
||||||
() => listPrivateMessages(beta),
|
|
||||||
e => !!e.private_messages[0],
|
|
||||||
);
|
|
||||||
expect(betaPms.private_messages[0].private_message.content).toBeDefined();
|
|
||||||
expect(betaPms.private_messages[0].private_message.local).toBe(false);
|
|
||||||
expect(betaPms.private_messages[0].creator.local).toBe(false);
|
|
||||||
expect(betaPms.private_messages[0].recipient.local).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Update a private message", async () => {
|
|
||||||
let updatedContent = "A jest test federated private message edited";
|
|
||||||
|
|
||||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
|
||||||
let pmUpdated = await editPrivateMessage(
|
|
||||||
alpha,
|
|
||||||
pmRes.private_message_view.private_message.id,
|
|
||||||
);
|
|
||||||
expect(pmUpdated.private_message_view.private_message.content).toBe(
|
|
||||||
updatedContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
let betaPms = await waitUntil(
|
|
||||||
() => listPrivateMessages(beta),
|
|
||||||
p => p.private_messages[0].private_message.content === updatedContent,
|
|
||||||
);
|
|
||||||
expect(betaPms.private_messages[0].private_message.content).toBe(
|
|
||||||
updatedContent,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Delete a private message", 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 deletedPmRes = await deletePrivateMessage(
|
|
||||||
alpha,
|
|
||||||
true,
|
|
||||||
pmRes.private_message_view.private_message.id,
|
|
||||||
);
|
|
||||||
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
|
|
||||||
|
|
||||||
// The GetPrivateMessages filters out deleted,
|
|
||||||
// even though they are in the actual database.
|
|
||||||
// no reason to show them
|
|
||||||
let betaPms2 = await waitUntil(
|
|
||||||
() => listPrivateMessages(beta),
|
|
||||||
p => p.private_messages.length === betaPms1.private_messages.length - 1,
|
|
||||||
);
|
|
||||||
expect(betaPms2.private_messages.length).toBe(
|
|
||||||
betaPms1.private_messages.length - 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Undelete
|
|
||||||
let undeletedPmRes = await deletePrivateMessage(
|
|
||||||
alpha,
|
|
||||||
false,
|
|
||||||
pmRes.private_message_view.private_message.id,
|
|
||||||
);
|
|
||||||
expect(undeletedPmRes.private_message_view.private_message.deleted).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
let betaPms3 = await waitUntil(
|
|
||||||
() => listPrivateMessages(beta),
|
|
||||||
p => p.private_messages.length === betaPms1.private_messages.length,
|
|
||||||
);
|
|
||||||
expect(betaPms3.private_messages.length).toBe(
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
|
1028
api_tests/src/shared.ts
vendored
1028
api_tests/src/shared.ts
vendored
File diff suppressed because it is too large
Load diff
225
api_tests/src/user.spec.ts
vendored
225
api_tests/src/user.spec.ts
vendored
|
@ -1,225 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import { PersonView } from "lemmy-js-client/dist/types/PersonView";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
registerUser,
|
|
||||||
resolvePerson,
|
|
||||||
getSite,
|
|
||||||
createPost,
|
|
||||||
resolveCommunity,
|
|
||||||
createComment,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
deleteUser,
|
|
||||||
saveUserSettingsFederated,
|
|
||||||
setupLogins,
|
|
||||||
alphaUrl,
|
|
||||||
saveUserSettings,
|
|
||||||
getPost,
|
|
||||||
getComments,
|
|
||||||
fetchFunction,
|
|
||||||
alphaImage,
|
|
||||||
unfollows,
|
|
||||||
getMyUser,
|
|
||||||
getPersonDetails,
|
|
||||||
} from "./shared";
|
|
||||||
import {
|
|
||||||
EditSite,
|
|
||||||
LemmyHttp,
|
|
||||||
SaveUserSettings,
|
|
||||||
UploadImage,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
let apShortname: string;
|
|
||||||
|
|
||||||
function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) {
|
|
||||||
expect(userOne?.person.name).toBe(userTwo?.person.name);
|
|
||||||
expect(userOne?.person.display_name).toBe(userTwo?.person.display_name);
|
|
||||||
expect(userOne?.person.bio).toBe(userTwo?.person.bio);
|
|
||||||
expect(userOne?.person.actor_id).toBe(userTwo?.person.actor_id);
|
|
||||||
expect(userOne?.person.avatar).toBe(userTwo?.person.avatar);
|
|
||||||
expect(userOne?.person.banner).toBe(userTwo?.person.banner);
|
|
||||||
expect(userOne?.person.published).toBe(userTwo?.person.published);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Create user", async () => {
|
|
||||||
let user = await registerUser(alpha, alphaUrl);
|
|
||||||
|
|
||||||
let my_user = await getMyUser(user);
|
|
||||||
expect(my_user).toBeDefined();
|
|
||||||
apShortname = `${my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Set some user settings, check that they are federated", async () => {
|
|
||||||
await saveUserSettingsFederated(alpha);
|
|
||||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
|
||||||
let betaPerson = (await resolvePerson(beta, apShortname)).person;
|
|
||||||
assertUserFederation(alphaPerson, betaPerson);
|
|
||||||
|
|
||||||
// Catches a bug where when only the person or local_user changed
|
|
||||||
let form: SaveUserSettings = {
|
|
||||||
theme: "test",
|
|
||||||
};
|
|
||||||
await saveUserSettings(beta, form);
|
|
||||||
|
|
||||||
let my_user = await getMyUser(beta);
|
|
||||||
expect(my_user.local_user_view.local_user.theme).toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Delete user", async () => {
|
|
||||||
let user = await registerUser(alpha, alphaUrl);
|
|
||||||
let user_profile = await getMyUser(user);
|
|
||||||
let person_id = user_profile.local_user_view.person.id;
|
|
||||||
|
|
||||||
// make a local post and comment
|
|
||||||
let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541"))
|
|
||||||
.community;
|
|
||||||
if (!alphaCommunity) {
|
|
||||||
throw "Missing alpha community";
|
|
||||||
}
|
|
||||||
let localPost = (await createPost(user, alphaCommunity.community.id))
|
|
||||||
.post_view.post;
|
|
||||||
expect(localPost).toBeDefined();
|
|
||||||
let localComment = (await createComment(user, localPost.id)).comment_view
|
|
||||||
.comment;
|
|
||||||
expect(localComment).toBeDefined();
|
|
||||||
|
|
||||||
// make a remote post and comment
|
|
||||||
let betaCommunity = (await resolveBetaCommunity(user)).community;
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let remotePost = (await createPost(user, betaCommunity.community.id))
|
|
||||||
.post_view.post;
|
|
||||||
expect(remotePost).toBeDefined();
|
|
||||||
let remoteComment = (await createComment(user, remotePost.id)).comment_view
|
|
||||||
.comment;
|
|
||||||
expect(remoteComment).toBeDefined();
|
|
||||||
|
|
||||||
await deleteUser(user);
|
|
||||||
await expect(getMyUser(user)).rejects.toStrictEqual(Error("incorrect_login"));
|
|
||||||
await expect(getPersonDetails(user, person_id)).rejects.toStrictEqual(
|
|
||||||
Error("not_found"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
await expect(
|
|
||||||
getPersonDetails(user, remoteComment.creator_id),
|
|
||||||
).rejects.toStrictEqual(Error("not_found"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Requests with invalid auth should be treated as unauthenticated", async () => {
|
|
||||||
let invalid_auth = new LemmyHttp(alphaUrl, {
|
|
||||||
headers: { Authorization: "Bearer foobar" },
|
|
||||||
fetchFunction,
|
|
||||||
});
|
|
||||||
await expect(getMyUser(invalid_auth)).rejects.toStrictEqual(
|
|
||||||
Error("incorrect_login"),
|
|
||||||
);
|
|
||||||
let site = await getSite(invalid_auth);
|
|
||||||
expect(site.site_view).toBeDefined();
|
|
||||||
|
|
||||||
let form: GetPosts = {};
|
|
||||||
let posts = invalid_auth.getPosts(form);
|
|
||||||
expect((await posts).posts).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create user with Arabic name", async () => {
|
|
||||||
// less than actor_name_max_length
|
|
||||||
const name = "تجريب" + Math.random().toString().slice(2, 10);
|
|
||||||
let user = await registerUser(alpha, alphaUrl, name);
|
|
||||||
|
|
||||||
let my_user = await getMyUser(user);
|
|
||||||
expect(my_user).toBeDefined();
|
|
||||||
apShortname = `${my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
|
||||||
|
|
||||||
let betaPerson1 = (await resolvePerson(beta, apShortname)).person;
|
|
||||||
expect(betaPerson1!.person.name).toBe(name);
|
|
||||||
|
|
||||||
let betaPerson2 = await getPersonDetails(beta, betaPerson1!.person.id);
|
|
||||||
expect(betaPerson2!.person_view.person.name).toBe(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create user with accept-language", async () => {
|
|
||||||
const edit: EditSite = {
|
|
||||||
discussion_languages: [32],
|
|
||||||
};
|
|
||||||
await alpha.editSite(edit);
|
|
||||||
|
|
||||||
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, *;q=0.5" },
|
|
||||||
});
|
|
||||||
let user = await registerUser(lemmy_http, alphaUrl);
|
|
||||||
|
|
||||||
let my_user = await getMyUser(user);
|
|
||||||
expect(my_user).toBeDefined();
|
|
||||||
expect(my_user?.local_user_view.local_user.interface_language).toBe("fr");
|
|
||||||
let site = await getSite(user);
|
|
||||||
let langs = site.all_languages
|
|
||||||
.filter(a => 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"),
|
|
||||||
};
|
|
||||||
await alpha.uploadUserAvatar(upload_form1);
|
|
||||||
const listMediaRes1 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes1.images.length).toBe(1);
|
|
||||||
|
|
||||||
let my_user1 = await alpha.getMyUser();
|
|
||||||
expect(my_user1.local_user_view.person.avatar).toBeDefined();
|
|
||||||
|
|
||||||
const upload_form2: UploadImage = {
|
|
||||||
image: Buffer.from("test2"),
|
|
||||||
};
|
|
||||||
await alpha.uploadUserAvatar(upload_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 alpha.uploadUserAvatar(upload_form2);
|
|
||||||
// make sure only the new avatar is kept
|
|
||||||
const listMediaRes3 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes3.images.length).toBe(1);
|
|
||||||
|
|
||||||
// make sure only the new avatar is kept
|
|
||||||
const listMediaRes4 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes4.images.length).toBe(1);
|
|
||||||
|
|
||||||
// delete the avatar
|
|
||||||
await alpha.deleteUserAvatar();
|
|
||||||
// make sure only the new avatar is kept
|
|
||||||
const listMediaRes5 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes5.images.length).toBe(0);
|
|
||||||
let my_user2 = await alpha.getMyUser();
|
|
||||||
expect(my_user2.local_user_view.person.avatar).toBeUndefined();
|
|
||||||
});
|
|
15
api_tests/tsconfig.json
vendored
15
api_tests/tsconfig.json
vendored
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": true,
|
|
||||||
"declarationDir": "./dist",
|
|
||||||
"module": "CommonJS",
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"lib": ["es2017", "es7", "es6", "dom"],
|
|
||||||
"outDir": "./dist",
|
|
||||||
"target": "ES2020",
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"moduleResolution": "Node"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
89
cliff.toml
vendored
89
cliff.toml
vendored
|
@ -1,89 +0,0 @@
|
||||||
# git-cliff ~ configuration file
|
|
||||||
# https://git-cliff.org/docs/configuration
|
|
||||||
|
|
||||||
[remote.github]
|
|
||||||
owner = "LemmyNet"
|
|
||||||
repo = "lemmy"
|
|
||||||
# token = ""
|
|
||||||
|
|
||||||
[changelog]
|
|
||||||
# template for the changelog body
|
|
||||||
# https://keats.github.io/tera/docs/#introduction
|
|
||||||
body = """
|
|
||||||
## What's Changed
|
|
||||||
|
|
||||||
{%- if version %} in {{ version }}{%- endif -%}
|
|
||||||
{% for commit in commits %}
|
|
||||||
{% if commit.github.pr_title -%}
|
|
||||||
{%- set commit_message = commit.github.pr_title -%}
|
|
||||||
{%- else -%}
|
|
||||||
{%- set commit_message = commit.message -%}
|
|
||||||
{%- endif -%}
|
|
||||||
* {{ commit_message | split(pat="\n") | first | trim }}\
|
|
||||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
|
|
||||||
{% if commit.github.pr_number %} in \
|
|
||||||
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
|
|
||||||
{%- if github -%}
|
|
||||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
|
||||||
{% raw %}\n{% endraw -%}
|
|
||||||
## New Contributors
|
|
||||||
{%- endif %}\
|
|
||||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
|
||||||
* @{{ contributor.username }} made their first contribution
|
|
||||||
{%- if contributor.pr_number %} in \
|
|
||||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
|
||||||
{%- endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endif -%}
|
|
||||||
|
|
||||||
{% if version %}
|
|
||||||
{% if previous.version %}
|
|
||||||
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
|
|
||||||
{% endif %}
|
|
||||||
{% else -%}
|
|
||||||
{% raw %}\n{% endraw %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{%- macro remote_url() -%}
|
|
||||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
|
||||||
{%- endmacro -%}
|
|
||||||
"""
|
|
||||||
# remove the leading and trailing whitespace from the template
|
|
||||||
trim = true
|
|
||||||
# changelog footer
|
|
||||||
footer = """
|
|
||||||
<!-- generated by git-cliff -->
|
|
||||||
"""
|
|
||||||
# postprocessors
|
|
||||||
postprocessors = []
|
|
||||||
|
|
||||||
[git]
|
|
||||||
# parse the commits based on https://www.conventionalcommits.org
|
|
||||||
conventional_commits = false
|
|
||||||
# filter out the commits that are not conventional
|
|
||||||
filter_unconventional = true
|
|
||||||
# process each line of a commit as an individual commit
|
|
||||||
split_commits = false
|
|
||||||
# regex for preprocessing the commit messages
|
|
||||||
commit_preprocessors = [
|
|
||||||
# remove issue numbers from commits
|
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
|
||||||
]
|
|
||||||
commit_parsers = [{ field = "author.name", pattern = "renovate", skip = true }]
|
|
||||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
|
||||||
protect_breaking_commits = false
|
|
||||||
# filter out the commits that are not matched by commit parsers
|
|
||||||
filter_commits = false
|
|
||||||
# regex for matching git tags
|
|
||||||
tag_pattern = "[0-9].*"
|
|
||||||
# regex for skipping tags
|
|
||||||
skip_tags = "beta|alpha"
|
|
||||||
# regex for ignoring tags
|
|
||||||
ignore_tags = "rc"
|
|
||||||
# sort the tags topologically
|
|
||||||
topo_order = false
|
|
||||||
# sort the commits inside sections by oldest/newest order
|
|
||||||
sort_commits = "newest"
|
|
5
config/config.hjson
vendored
5
config/config.hjson
vendored
|
@ -1,5 +0,0 @@
|
||||||
# See the documentation for available config fields and descriptions:
|
|
||||||
# https://join-lemmy.org/docs/en/administration/configuration.html
|
|
||||||
{
|
|
||||||
hostname: lemmy-alpha
|
|
||||||
}
|
|
116
config/defaults.hjson
vendored
116
config/defaults.hjson
vendored
|
@ -1,116 +0,0 @@
|
||||||
{
|
|
||||||
# settings related to the postgresql database
|
|
||||||
database: {
|
|
||||||
# Configure the database by specifying URI pointing to a postgres instance
|
|
||||||
#
|
|
||||||
# This example uses peer authentication to obviate the need for creating,
|
|
||||||
# configuring, and managing passwords.
|
|
||||||
#
|
|
||||||
# For an explanation of how to use connection URIs, see [here][0] in
|
|
||||||
# PostgreSQL's documentation.
|
|
||||||
#
|
|
||||||
# [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6
|
|
||||||
connection: "postgres://lemmy:password@localhost:5432/lemmy"
|
|
||||||
# Maximum number of active sql connections
|
|
||||||
pool_size: 30
|
|
||||||
}
|
|
||||||
# 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"
|
|
||||||
# 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 behaviour matches Lemmy 0.18.
|
|
||||||
"StoreLinkPreviews"
|
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
# If enabled, all images from remote domains are rewritten to pass through
|
|
||||||
# `/api/v4/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"
|
|
||||||
# Allows bypassing proxy for specific image hosts when using ProxyAllImages.
|
|
||||||
#
|
|
||||||
# imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass
|
|
||||||
# in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur
|
|
||||||
# requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`.
|
|
||||||
proxy_bypass_domains: [
|
|
||||||
"i.imgur.com"
|
|
||||||
/* ... */
|
|
||||||
]
|
|
||||||
# Timeout for uploading images to pictrs (in seconds)
|
|
||||||
upload_timeout: 30
|
|
||||||
# Resize post thumbnails to this maximum width/height.
|
|
||||||
max_thumbnail_size: 512
|
|
||||||
# Maximum size for user avatar, community icon and site icon.
|
|
||||||
max_avatar_size: 512
|
|
||||||
# Maximum size for user, community and site banner. Larger images are downscaled to fit
|
|
||||||
# into a square of this size.
|
|
||||||
max_banner_size: 1024
|
|
||||||
# Prevent users from uploading images for posts or embedding in markdown. Avatars, icons and
|
|
||||||
# banners can still be uploaded.
|
|
||||||
image_upload_disabled: false
|
|
||||||
}
|
|
||||||
# Email sending configuration. All options except login/password are mandatory
|
|
||||||
email: {
|
|
||||||
# Hostname and port of the smtp server
|
|
||||||
smtp_server: "localhost:25"
|
|
||||||
# Login name for smtp server
|
|
||||||
smtp_login: "string"
|
|
||||||
# Password to login to the smtp server
|
|
||||||
smtp_password: "string"
|
|
||||||
# Address to send emails from, eg "noreply@your-instance.com"
|
|
||||||
smtp_from_address: "noreply@example.com"
|
|
||||||
# Whether or not smtp connections should use tls. Can be none, tls, or starttls
|
|
||||||
tls_type: "none"
|
|
||||||
}
|
|
||||||
# Parameters for automatic configuration of new instance (only used at first start)
|
|
||||||
setup: {
|
|
||||||
# Username for the admin user
|
|
||||||
admin_username: "admin"
|
|
||||||
# Password for the admin user. It must be between 10 and 60 characters.
|
|
||||||
admin_password: "tf6HHDS4RolWfFhk4Rq9"
|
|
||||||
# Name of the site, can be changed later. Maximum 20 characters.
|
|
||||||
site_name: "My Lemmy Instance"
|
|
||||||
# Email for the admin user (optional, can be omitted and set later through the website)
|
|
||||||
admin_email: "user@example.com"
|
|
||||||
}
|
|
||||||
# the domain name of your instance (mandatory)
|
|
||||||
hostname: "unset"
|
|
||||||
# Address where lemmy should listen for incoming requests
|
|
||||||
bind: "0.0.0.0"
|
|
||||||
# Port where lemmy should listen for incoming requests
|
|
||||||
port: 8536
|
|
||||||
# Whether the site is available over TLS. Needs to be true for federation to work.
|
|
||||||
tls_enabled: true
|
|
||||||
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: "lemmy.tld"
|
|
||||||
}
|
|
46
crates/api/Cargo.toml
vendored
46
crates/api/Cargo.toml
vendored
|
@ -1,46 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "lemmy_api"
|
|
||||||
publish = false
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
description.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
documentation.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "lemmy_api"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
lemmy_utils = { workspace = true }
|
|
||||||
lemmy_db_schema = { workspace = true, features = ["full"] }
|
|
||||||
lemmy_db_views = { workspace = true, features = ["full"] }
|
|
||||||
lemmy_db_views_moderator = { workspace = true, features = ["full"] }
|
|
||||||
lemmy_db_views_actor = { workspace = true, features = ["full"] }
|
|
||||||
lemmy_api_common = { workspace = true, features = ["full"] }
|
|
||||||
activitypub_federation = { workspace = true }
|
|
||||||
bcrypt = { workspace = true }
|
|
||||||
actix-web = { workspace = true }
|
|
||||||
base64 = { workspace = true }
|
|
||||||
captcha = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
hound = "3.5.1"
|
|
||||||
sitemap-rs = "0.2.2"
|
|
||||||
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 }
|
|
|
@ -1,68 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{CommentResponse, DistinguishComment},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::{check_community_mod_action, check_community_user_action},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::comment::{Comment, CommentUpdateForm},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
|
||||||
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,
|
|
||||||
) -> 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,
|
|
||||||
&orig_comment.community,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.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,
|
|
||||||
&orig_comment.community,
|
|
||||||
false,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Update the Comment
|
|
||||||
let form = CommentUpdateForm {
|
|
||||||
distinguished: Some(data.distinguished),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
Comment::update(&mut context.pool(), data.comment_id, &form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
|
||||||
|
|
||||||
let comment_view = CommentView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
data.comment_id,
|
|
||||||
Some(&local_user_view.local_user),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids: Vec::new(),
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
build_response::build_comment_response,
|
|
||||||
comment::{CommentResponse, CreateCommentLike},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
newtypes::LocalUserId,
|
|
||||||
source::{
|
|
||||||
comment::{CommentLike, CommentLikeForm},
|
|
||||||
comment_reply::CommentReply,
|
|
||||||
local_site::LocalSite,
|
|
||||||
},
|
|
||||||
traits::Likeable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn like_comment(
|
|
||||||
data: Json<CreateCommentLike>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> 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();
|
|
||||||
|
|
||||||
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 orig_comment = CommentView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
comment_id,
|
|
||||||
Some(&local_user_view.local_user),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
&orig_comment.community,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Add parent poster or commenter to recipients
|
|
||||||
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
|
|
||||||
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
|
|
||||||
{
|
|
||||||
recipient_ids.push(local_recipient.local_user.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let like_form = CommentLikeForm {
|
|
||||||
comment_id: data.comment_id,
|
|
||||||
person_id: local_user_view.person.id,
|
|
||||||
score: data.score,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove any likes first
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
CommentLike::remove(&mut context.pool(), person_id, comment_id).await?;
|
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
|
||||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
|
||||||
if do_add {
|
|
||||||
CommentLike::like(&mut context.pool(), &like_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::LikePostOrComment {
|
|
||||||
object_id: orig_comment.comment.ap_id,
|
|
||||||
actor: local_user_view.person.clone(),
|
|
||||||
community: orig_comment.community,
|
|
||||||
score: data.score,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(
|
|
||||||
build_comment_response(
|
|
||||||
context.deref(),
|
|
||||||
comment_id,
|
|
||||||
Some(local_user_view),
|
|
||||||
recipient_ids,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{ListCommentLikes, ListCommentLikesResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::is_mod_or_admin,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
/// 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,4 +0,0 @@
|
||||||
pub mod distinguish;
|
|
||||||
pub mod like;
|
|
||||||
pub mod list_comment_likes;
|
|
||||||
pub mod save;
|
|
|
@ -1,43 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{CommentResponse, SaveComment},
|
|
||||||
context::LemmyContext,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::comment::{CommentSaved, CommentSavedForm},
|
|
||||||
traits::Saveable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView};
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<CommentResponse>> {
|
|
||||||
let comment_saved_form = CommentSavedForm::new(data.comment_id, local_user_view.person.id);
|
|
||||||
|
|
||||||
if data.save {
|
|
||||||
CommentSaved::save(&mut context.pool(), &comment_saved_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
|
|
||||||
} else {
|
|
||||||
CommentSaved::unsave(&mut context.pool(), &comment_saved_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntSaveComment)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
|
||||||
let comment_view = CommentView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
comment_id,
|
|
||||||
Some(&local_user_view.local_user),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids: Vec::new(),
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{AddModToCommunity, AddModToCommunityResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::check_community_mod_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
|
||||||
local_user::LocalUser,
|
|
||||||
mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
|
|
||||||
},
|
|
||||||
traits::{Crud, Joinable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<AddModToCommunityResponse>> {
|
|
||||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
|
||||||
// Verify that only mods or admins can add mod
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
&community,
|
|
||||||
false,
|
|
||||||
&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?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
CommunityModeratorView::check_is_community_moderator(
|
|
||||||
&mut context.pool(),
|
|
||||||
community.id,
|
|
||||||
local_user_view.person.id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in local database
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
|
||||||
community_id: data.community_id,
|
|
||||||
person_id: data.person_id,
|
|
||||||
};
|
|
||||||
if data.added {
|
|
||||||
CommunityModerator::join(&mut context.pool(), &community_moderator_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
|
||||||
} else {
|
|
||||||
CommunityModerator::leave(&mut context.pool(), &community_moderator_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mod tables
|
|
||||||
let form = ModAddCommunityForm {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
other_person_id: data.person_id,
|
|
||||||
community_id: data.community_id,
|
|
||||||
removed: Some(!data.added),
|
|
||||||
};
|
|
||||||
|
|
||||||
ModAddCommunity::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
// Note: in case a remote mod is added, this returns the old moderators list, it will only get
|
|
||||||
// updated once we receive an activity from the community (like `Announce/Add/Moderator`)
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::AddModToCommunity {
|
|
||||||
moderator: local_user_view.person,
|
|
||||||
community_id: data.community_id,
|
|
||||||
target: data.person_id,
|
|
||||||
added: data.added,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(AddModToCommunityResponse { moderators }))
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{BanFromCommunity, BanFromCommunityResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{
|
|
||||||
check_community_mod_action,
|
|
||||||
check_expire_time,
|
|
||||||
remove_or_restore_user_data_in_community,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{
|
|
||||||
Community,
|
|
||||||
CommunityFollower,
|
|
||||||
CommunityFollowerForm,
|
|
||||||
CommunityPersonBan,
|
|
||||||
CommunityPersonBanForm,
|
|
||||||
},
|
|
||||||
local_user::LocalUser,
|
|
||||||
mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
|
||||||
},
|
|
||||||
traits::{Bannable, Crud, Followable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::PersonView;
|
|
||||||
use lemmy_utils::{
|
|
||||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
|
||||||
utils::validation::is_valid_body_field,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn ban_from_community(
|
|
||||||
data: Json<BanFromCommunity>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<BanFromCommunityResponse>> {
|
|
||||||
let banned_person_id = data.person_id;
|
|
||||||
let expires = check_expire_time(data.expires)?;
|
|
||||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
|
||||||
|
|
||||||
// Verify that only mods or admins can ban
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
&community,
|
|
||||||
false,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
person_id: data.person_id,
|
|
||||||
expires: Some(expires),
|
|
||||||
};
|
|
||||||
|
|
||||||
if data.ban {
|
|
||||||
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
|
|
||||||
|
|
||||||
// Also unsubscribe them from the community, if they are subscribed
|
|
||||||
let community_follower_form = CommunityFollowerForm::new(data.community_id, banned_person_id);
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityUserAlreadyBanned)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove/Restore their data if that's desired
|
|
||||||
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 {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
other_person_id: data.person_id,
|
|
||||||
community_id: data.community_id,
|
|
||||||
reason: data.reason.clone(),
|
|
||||||
banned: Some(data.ban),
|
|
||||||
expires,
|
|
||||||
};
|
|
||||||
|
|
||||||
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
let person_view = PersonView::read(&mut context.pool(), data.person_id, false).await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::BanFromCommunity {
|
|
||||||
moderator: local_user_view.person,
|
|
||||||
community_id: data.community_id,
|
|
||||||
target: person_view.person.clone(),
|
|
||||||
data: data.0.clone(),
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(BanFromCommunityResponse {
|
|
||||||
person_view,
|
|
||||||
banned: data.ban,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{BlockCommunity, BlockCommunityResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{CommunityFollower, CommunityFollowerForm},
|
|
||||||
community_block::{CommunityBlock, CommunityBlockForm},
|
|
||||||
},
|
|
||||||
traits::{Blockable, Followable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::CommunityView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn user_block_community(
|
|
||||||
data: Json<BlockCommunity>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<BlockCommunityResponse>> {
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let community_block_form = CommunityBlockForm {
|
|
||||||
person_id,
|
|
||||||
community_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if data.block {
|
|
||||||
CommunityBlock::block(&mut context.pool(), &community_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
|
||||||
|
|
||||||
// Also, unfollow the community, and send a federated unfollow
|
|
||||||
let community_follower_form = CommunityFollowerForm::new(data.community_id, person_id);
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
CommunityBlock::unblock(&mut context.pool(), &community_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let community_view = CommunityView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
community_id,
|
|
||||||
Some(&local_user_view.local_user),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::FollowCommunity(
|
|
||||||
community_view.community.clone(),
|
|
||||||
local_user_view.person.clone(),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(BlockCommunityResponse {
|
|
||||||
blocked: data.block,
|
|
||||||
community_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{CommunityResponse, FollowCommunity},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{check_community_deleted_removed, check_user_valid},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
actor_language::CommunityLanguage,
|
|
||||||
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityFollowerState},
|
|
||||||
},
|
|
||||||
traits::{Crud, Followable},
|
|
||||||
CommunityVisibility,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<CommunityResponse>> {
|
|
||||||
check_user_valid(&local_user_view.person)?;
|
|
||||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
|
||||||
let form = CommunityFollowerForm::new(community.id, local_user_view.person.id);
|
|
||||||
|
|
||||||
if data.follow {
|
|
||||||
// Only run these checks for local community, in case of remote community the local
|
|
||||||
// state may be outdated. Can't use check_community_user_action() here as it only allows
|
|
||||||
// actions from existing followers for private community (so following would be impossible).
|
|
||||||
if community.local {
|
|
||||||
check_community_deleted_removed(&community)?;
|
|
||||||
CommunityPersonBanView::check(&mut context.pool(), local_user_view.person.id, community.id)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = if community.local {
|
|
||||||
// Local follow is accepted immediately
|
|
||||||
Some(CommunityFollowerState::Accepted)
|
|
||||||
} else if community.visibility == CommunityVisibility::Private {
|
|
||||||
// Private communities require manual approval
|
|
||||||
Some(CommunityFollowerState::ApprovalRequired)
|
|
||||||
} else {
|
|
||||||
// remote follow needs to be federated first
|
|
||||||
Some(CommunityFollowerState::Pending)
|
|
||||||
};
|
|
||||||
|
|
||||||
let form = CommunityFollowerForm {
|
|
||||||
state,
|
|
||||||
..CommunityFollowerForm::new(community.id, local_user_view.person.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write to db
|
|
||||||
CommunityFollower::follow(&mut context.pool(), &form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
|
||||||
} else {
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the federated follow
|
|
||||||
if !community.local {
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
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 {
|
|
||||||
community_view,
|
|
||||||
discussion_languages,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::HideCommunity,
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::is_admin,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{Community, CommunityUpdateForm},
|
|
||||||
mod_log::moderator::{ModHideCommunity, ModHideCommunityForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
// Verify its a admin (only admin can hide or unhide it)
|
|
||||||
is_admin(&local_user_view)?;
|
|
||||||
|
|
||||||
let community_form = CommunityUpdateForm {
|
|
||||||
hidden: Some(data.hidden),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mod_hide_community_form = ModHideCommunityForm {
|
|
||||||
community_id: data.community_id,
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
reason: data.reason.clone(),
|
|
||||||
hidden: Some(data.hidden),
|
|
||||||
};
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let community = Community::update(&mut context.pool(), community_id, &community_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateCommunityHiddenStatus)?;
|
|
||||||
|
|
||||||
ModHideCommunity::create(&mut context.pool(), &mod_hide_community_form).await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::UpdateCommunity(local_user_view.person.clone(), community),
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
pub mod add_mod;
|
|
||||||
pub mod ban;
|
|
||||||
pub mod block;
|
|
||||||
pub mod follow;
|
|
||||||
pub mod hide;
|
|
||||||
pub mod pending_follows;
|
|
||||||
pub mod random;
|
|
||||||
pub mod transfer;
|
|
|
@ -1,46 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::ApproveCommunityPendingFollower,
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::is_mod_or_admin,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::community::{CommunityFollower, CommunityFollowerForm},
|
|
||||||
traits::Followable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn post_pending_follows_approve(
|
|
||||||
data: Json<ApproveCommunityPendingFollower>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
is_mod_or_admin(
|
|
||||||
&mut context.pool(),
|
|
||||||
&local_user_view.person,
|
|
||||||
data.community_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let activity_data = if data.approve {
|
|
||||||
CommunityFollower::approve(
|
|
||||||
&mut context.pool(),
|
|
||||||
data.community_id,
|
|
||||||
data.follower_id,
|
|
||||||
local_user_view.person.id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
SendActivityData::AcceptFollower(data.community_id, data.follower_id)
|
|
||||||
} else {
|
|
||||||
let form = CommunityFollowerForm::new(data.community_id, data.follower_id);
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &form).await?;
|
|
||||||
SendActivityData::RejectFollower(data.community_id, data.follower_id)
|
|
||||||
};
|
|
||||||
ActivityChannel::submit_activity(activity_data, &context)?;
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{GetCommunityPendingFollowsCount, GetCommunityPendingFollowsCountResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::is_mod_or_admin,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::CommunityFollowerView;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn get_pending_follows_count(
|
|
||||||
data: Query<GetCommunityPendingFollowsCount>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetCommunityPendingFollowsCountResponse>> {
|
|
||||||
is_mod_or_admin(
|
|
||||||
&mut context.pool(),
|
|
||||||
&local_user_view.person,
|
|
||||||
data.community_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let count =
|
|
||||||
CommunityFollowerView::count_approval_required(&mut context.pool(), data.community_id).await?;
|
|
||||||
Ok(Json(GetCommunityPendingFollowsCountResponse { count }))
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{ListCommunityPendingFollows, ListCommunityPendingFollowsResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::check_community_mod_of_any_or_admin_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::CommunityFollowerView;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn get_pending_follows_list(
|
|
||||||
data: Query<ListCommunityPendingFollows>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListCommunityPendingFollowsResponse>> {
|
|
||||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
|
||||||
let all_communities =
|
|
||||||
data.all_communities.unwrap_or_default() && local_user_view.local_user.admin;
|
|
||||||
let items = CommunityFollowerView::list_approval_required(
|
|
||||||
&mut context.pool(),
|
|
||||||
local_user_view.person.id,
|
|
||||||
all_communities,
|
|
||||||
data.pending_only.unwrap_or_default(),
|
|
||||||
data.page,
|
|
||||||
data.limit,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ListCommunityPendingFollowsResponse { items }))
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod approve;
|
|
||||||
pub mod count;
|
|
||||||
pub mod list;
|
|
|
@ -1,55 +0,0 @@
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use anyhow::Context;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
community::{GetCommunityResponse, TransferCommunity},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::{check_community_user_action, is_admin, is_top_mod},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
|
||||||
mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm},
|
|
||||||
},
|
|
||||||
traits::{Crud, Joinable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
|
|
||||||
use lemmy_utils::{
|
|
||||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
|
||||||
location_info,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: we dont do anything for federation here, it should be updated the next time the community
|
|
||||||
// gets fetched. i hope we can get rid of the community creator role soon.
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn transfer_community(
|
|
||||||
data: Json<TransferCommunity>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetCommunityResponse>> {
|
|
||||||
let community = Community::read(&mut context.pool(), data.community_id).await?;
|
|
||||||
let mut community_mods =
|
|
||||||
CommunityModeratorView::for_community(&mut context.pool(), community.id).await?;
|
|
||||||
|
|
||||||
check_community_user_action(&local_user_view.person, &community, &mut context.pool()).await?;
|
|
||||||
|
|
||||||
// Make sure transferrer is either the top community mod, or an admin
|
|
||||||
if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok())
|
|
||||||
{
|
|
||||||
Err(LemmyErrorType::NotAnAdmin)?
|
|
||||||
}
|
|
||||||
|
|
||||||
// You have to re-do the community_moderator table, reordering it.
|
|
||||||
// Add the transferee to the top
|
|
||||||
let creator_index = community_mods
|
|
||||||
.iter()
|
|
||||||
.position(|r| r.moderator.id == data.person_id)
|
|
||||||
.context(location_info!())?;
|
|
||||||
let creator_person = community_mods.remove(creator_index);
|
|
||||||
community_mods.insert(0, creator_person);
|
|
||||||
|
|
||||||
// Delete all the mods
|
|
||||||
let community_id = data.community_id;
|
|
||||||
|
|
||||||
CommunityModerator::delete_for_community(&mut context.pool(), community_id).await?;
|
|
||||||
|
|
||||||
// TODO: this should probably be a bulk operation
|
|
||||||
// Re-add the mods, in the new order
|
|
||||||
for cmod in &community_mods {
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
|
||||||
community_id: cmod.community.id,
|
|
||||||
person_id: cmod.moderator.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
CommunityModerator::join(&mut context.pool(), &community_moderator_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityModeratorAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mod tables
|
|
||||||
let form = ModTransferCommunityForm {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
other_person_id: data.person_id,
|
|
||||||
community_id: data.community_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
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?;
|
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(Json(GetCommunityResponse {
|
|
||||||
community_view,
|
|
||||||
site: None,
|
|
||||||
moderators,
|
|
||||||
discussion_languages: vec![],
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
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,
|
|
||||||
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,
|
|
||||||
mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
|
||||||
person::Person,
|
|
||||||
},
|
|
||||||
traits::{Bannable, Crud, Followable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::{
|
|
||||||
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
|
||||||
utils::slurs::check_slurs,
|
|
||||||
};
|
|
||||||
use std::io::Cursor;
|
|
||||||
use totp_rs::{Secret, TOTP};
|
|
||||||
|
|
||||||
pub mod comment;
|
|
||||||
pub mod community;
|
|
||||||
pub mod local_user;
|
|
||||||
pub mod post;
|
|
||||||
pub mod private_message;
|
|
||||||
pub mod reports;
|
|
||||||
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) -> 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<hound::WavSpec> = None;
|
|
||||||
for letter in letters {
|
|
||||||
let mut cursor = Cursor::new(letter.unwrap_or_default());
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode the concatenated result as a wav file
|
|
||||||
let mut output_buffer = Cursor::new(vec![]);
|
|
||||||
if let Some(header) = any_header {
|
|
||||||
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()))
|
|
||||||
} else {
|
|
||||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check size of report
|
|
||||||
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)?;
|
|
||||||
if reason.is_empty() {
|
|
||||||
Err(LemmyErrorType::ReportReasonRequired)?
|
|
||||||
} else if reason.chars().count() > 1000 {
|
|
||||||
Err(LemmyErrorType::ReportTooLong)?
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
Ok(Some(cookie.value().to_string()))
|
|
||||||
}
|
|
||||||
// Otherwise, there's no auth
|
|
||||||
else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn check_totp_2fa_valid(
|
|
||||||
local_user_view: &LocalUserView,
|
|
||||||
totp_token: &Option<String>,
|
|
||||||
site_name: &str,
|
|
||||||
) -> LemmyResult<()> {
|
|
||||||
// Throw an error if their token is missing
|
|
||||||
let token = totp_token
|
|
||||||
.as_deref()
|
|
||||||
.ok_or(LemmyErrorType::MissingTotpToken)?;
|
|
||||||
let secret = local_user_view
|
|
||||||
.local_user
|
|
||||||
.totp_2fa_secret
|
|
||||||
.as_deref()
|
|
||||||
.ok_or(LemmyErrorType::MissingTotpSecret)?;
|
|
||||||
|
|
||||||
let totp = build_totp_2fa(site_name, &local_user_view.person.name, secret)?;
|
|
||||||
|
|
||||||
let check_passed = totp.check_current(token)?;
|
|
||||||
if !check_passed {
|
|
||||||
return Err(LemmyErrorType::IncorrectTotpToken.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn generate_totp_2fa_secret() -> String {
|
|
||||||
Secret::generate_secret().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntParseTotpSecret)?;
|
|
||||||
|
|
||||||
TOTP::new(
|
|
||||||
totp_rs::Algorithm::SHA1,
|
|
||||||
6,
|
|
||||||
1,
|
|
||||||
30,
|
|
||||||
sec_bytes,
|
|
||||||
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::new(community_id, target.id);
|
|
||||||
|
|
||||||
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,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn local_user_view_from_jwt(
|
|
||||||
jwt: &str,
|
|
||||||
context: &LemmyContext,
|
|
||||||
) -> LemmyResult<LocalUserView> {
|
|
||||||
let local_user_id = Claims::validate(jwt, context)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
|
|
||||||
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
|
|
||||||
check_user_valid(&local_user_view.person)?;
|
|
||||||
|
|
||||||
Ok(local_user_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_build_totp() {
|
|
||||||
let generated_secret = generate_totp_2fa_secret();
|
|
||||||
let totp = build_totp_2fa("lemmy.ml", "my_name", &generated_secret);
|
|
||||||
assert!(totp.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{AddAdmin, AddAdminResponse},
|
|
||||||
utils::is_admin,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
local_user::{LocalUser, LocalUserUpdateForm},
|
|
||||||
mod_log::moderator::{ModAdd, ModAddForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::PersonView;
|
|
||||||
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,
|
|
||||||
) -> 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)?;
|
|
||||||
|
|
||||||
LocalUser::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
added_local_user.local_user.id,
|
|
||||||
&LocalUserUpdateForm {
|
|
||||||
admin: Some(data.added),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
|
||||||
|
|
||||||
// Mod tables
|
|
||||||
let form = ModAddForm {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
other_person_id: added_local_user.person.id,
|
|
||||||
removed: Some(!data.added),
|
|
||||||
};
|
|
||||||
|
|
||||||
ModAdd::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
let admins = PersonView::admins(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
Ok(Json(AddAdminResponse { admins }))
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
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_or_restore_user_data},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
local_user::LocalUser,
|
|
||||||
login_token::LoginToken,
|
|
||||||
mod_log::moderator::{ModBan, ModBanForm},
|
|
||||||
person::{Person, PersonUpdateForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::PersonView;
|
|
||||||
use lemmy_utils::{
|
|
||||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
|
||||||
utils::validation::is_valid_body_field,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn ban_from_site(
|
|
||||||
data: Json<BanPerson>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<BanPersonResponse>> {
|
|
||||||
// Make sure user is an admin
|
|
||||||
is_admin(&local_user_view)?;
|
|
||||||
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
let person = Person::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
data.person_id,
|
|
||||||
&PersonUpdateForm {
|
|
||||||
banned: Some(data.ban),
|
|
||||||
ban_expires: Some(expires),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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: person.id,
|
|
||||||
reason: data.reason.clone(),
|
|
||||||
banned: Some(data.ban),
|
|
||||||
expires,
|
|
||||||
};
|
|
||||||
|
|
||||||
ModBan::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
let person_view = PersonView::read(&mut context.pool(), person.id, false).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 {
|
|
||||||
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,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Json(BanPersonResponse {
|
|
||||||
person_view,
|
|
||||||
banned: data.ban,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{BlockPerson, BlockPersonResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::person_block::{PersonBlock, PersonBlockForm},
|
|
||||||
traits::Blockable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::PersonView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn user_block_person(
|
|
||||||
data: Json<BlockPerson>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<BlockPersonResponse>> {
|
|
||||||
let target_id = data.person_id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
// Don't let a person block themselves
|
|
||||||
if target_id == person_id {
|
|
||||||
Err(LemmyErrorType::CantBlockYourself)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let person_block_form = PersonBlockForm {
|
|
||||||
person_id,
|
|
||||||
target_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
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)?
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.block {
|
|
||||||
PersonBlock::block(&mut context.pool(), &person_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
|
|
||||||
} else {
|
|
||||||
PersonBlock::unblock(&mut context.pool(), &person_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let person_view = PersonView::read(&mut context.pool(), target_id, false).await?;
|
|
||||||
Ok(Json(BlockPersonResponse {
|
|
||||||
person_view,
|
|
||||||
blocked: data.block,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
use actix_web::{
|
|
||||||
web::{Data, Json},
|
|
||||||
HttpRequest,
|
|
||||||
};
|
|
||||||
use bcrypt::verify;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
claims::Claims,
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{ChangePassword, LoginResponse},
|
|
||||||
utils::password_length_check,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{local_user::LocalUser, login_token::LoginToken};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn change_password(
|
|
||||||
data: Json<ChangePassword>,
|
|
||||||
req: HttpRequest,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<LoginResponse>> {
|
|
||||||
password_length_check(&data.new_password)?;
|
|
||||||
|
|
||||||
// Make sure passwords match
|
|
||||||
if data.new_password != data.new_password_verify {
|
|
||||||
Err(LemmyErrorType::PasswordsDoNotMatch)?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the old password
|
|
||||||
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)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_user_id = local_user_view.local_user.id;
|
|
||||||
let new_password = data.new_password.clone();
|
|
||||||
let updated_local_user =
|
|
||||||
LocalUser::update_password(&mut context.pool(), local_user_id, &new_password).await?;
|
|
||||||
|
|
||||||
LoginToken::invalidate_all(&mut context.pool(), local_user_view.local_user.id).await?;
|
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(Json(LoginResponse {
|
|
||||||
jwt: Some(Claims::generate(updated_local_user.id, req, &context).await?),
|
|
||||||
verify_email_sent: false,
|
|
||||||
registration_created: false,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::PasswordChangeAfterReset,
|
|
||||||
utils::password_length_check,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
local_user::LocalUser,
|
|
||||||
login_token::LoginToken,
|
|
||||||
password_reset_request::PasswordResetRequest,
|
|
||||||
};
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn change_password_after_reset(
|
|
||||||
data: Json<PasswordChangeAfterReset>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
// Fetch the user_id from the token
|
|
||||||
let token = data.token.clone();
|
|
||||||
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
|
|
||||||
.await?
|
|
||||||
.local_user_id;
|
|
||||||
|
|
||||||
password_length_check(&data.password)?;
|
|
||||||
|
|
||||||
// Make sure passwords match
|
|
||||||
if data.password != data.password_verify {
|
|
||||||
Err(LemmyErrorType::PasswordsDoNotMatch)?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the user with the new password
|
|
||||||
let password = data.password.clone();
|
|
||||||
LocalUser::update_password(&mut context.pool(), local_user_id, &password).await?;
|
|
||||||
|
|
||||||
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
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};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
local_user::{LocalUser, LocalUserUpdateForm},
|
|
||||||
site::Site,
|
|
||||||
};
|
|
||||||
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.
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn generate_totp_secret(
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> 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.name, &local_user_view.person.name, &secret)?.get_url();
|
|
||||||
|
|
||||||
let local_user_form = LocalUserUpdateForm {
|
|
||||||
totp_2fa_secret: Some(Some(secret)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
LocalUser::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
local_user_view.local_user.id,
|
|
||||||
&local_user_form,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(GenerateTotpSecretResponse {
|
|
||||||
totp_secret_url: secret_url.into(),
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
use crate::captcha_as_wav_base64;
|
|
||||||
use actix_web::{
|
|
||||||
http::{
|
|
||||||
header::{CacheControl, CacheDirective},
|
|
||||||
StatusCode,
|
|
||||||
},
|
|
||||||
web::{Data, Json},
|
|
||||||
HttpResponse,
|
|
||||||
HttpResponseBuilder,
|
|
||||||
};
|
|
||||||
use captcha::{gen, Difficulty};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{CaptchaResponse, GetCaptchaResponse},
|
|
||||||
LemmyErrorType,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
|
|
||||||
local_site::LocalSite,
|
|
||||||
};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
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(res.json(Json(GetCaptchaResponse { ok: None })));
|
|
||||||
}
|
|
||||||
|
|
||||||
let captcha = gen(match local_site.captcha_difficulty.as_str() {
|
|
||||||
"easy" => Difficulty::Easy,
|
|
||||||
"hard" => Difficulty::Hard,
|
|
||||||
_ => Difficulty::Medium,
|
|
||||||
});
|
|
||||||
|
|
||||||
let answer = captcha.chars_as_string();
|
|
||||||
|
|
||||||
let png = captcha
|
|
||||||
.as_base64()
|
|
||||||
.ok_or(LemmyErrorType::CouldntCreateImageCaptcha)?;
|
|
||||||
|
|
||||||
let wav = captcha_as_wav_base64(&captcha)?;
|
|
||||||
|
|
||||||
let captcha_form: CaptchaAnswerForm = CaptchaAnswerForm { answer };
|
|
||||||
// Stores the captcha item in the db
|
|
||||||
let captcha = CaptchaAnswer::insert(&mut context.pool(), &captcha_form).await?;
|
|
||||||
|
|
||||||
let json = Json(GetCaptchaResponse {
|
|
||||||
ok: Some(CaptchaResponse {
|
|
||||||
png,
|
|
||||||
wav,
|
|
||||||
uuid: captcha.uuid.to_string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
Ok(res.json(json))
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn list_banned_users(
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<BannedPersonsResponse>> {
|
|
||||||
// Make sure user is an admin
|
|
||||||
is_admin(&local_user_view)?;
|
|
||||||
|
|
||||||
let banned = PersonView::banned(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
Ok(Json(BannedPersonsResponse { banned }))
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
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::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn list_logins(
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListLoginsResponse>> {
|
|
||||||
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
|
|
||||||
|
|
||||||
Ok(Json(ListLoginsResponse { logins }))
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{ListMedia, ListMediaResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalImageView, LocalUserView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_media(
|
|
||||||
data: Query<ListMedia>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListMediaResponse>> {
|
|
||||||
let page = data.page;
|
|
||||||
let limit = data.limit;
|
|
||||||
let images = LocalImageView::get_all_paged_by_local_user_id(
|
|
||||||
&mut context.pool(),
|
|
||||||
local_user_view.local_user.id,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ListMediaResponse { images }))
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::{Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{ListPersonSaved, ListPersonSavedResponse},
|
|
||||||
utils::check_private_instance,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::{
|
|
||||||
person_saved_combined_view::PersonSavedCombinedQuery,
|
|
||||||
structs::{LocalUserView, SiteView},
|
|
||||||
};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_person_saved(
|
|
||||||
data: Query<ListPersonSaved>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListPersonSavedResponse>> {
|
|
||||||
let local_site = SiteView::read_local(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
check_private_instance(&Some(local_user_view.clone()), &local_site.local_site)?;
|
|
||||||
|
|
||||||
// parse pagination token
|
|
||||||
let page_after = if let Some(pa) = &data.page_cursor {
|
|
||||||
Some(pa.read(&mut context.pool()).await?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let page_back = data.page_back;
|
|
||||||
let type_ = data.type_;
|
|
||||||
|
|
||||||
let saved = PersonSavedCombinedQuery {
|
|
||||||
type_,
|
|
||||||
page_after,
|
|
||||||
page_back,
|
|
||||||
}
|
|
||||||
.list(&mut context.pool(), &local_user_view)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(ListPersonSavedResponse { saved }))
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
use crate::check_totp_2fa_valid;
|
|
||||||
use actix_web::{
|
|
||||||
web::{Data, Json},
|
|
||||||
HttpRequest,
|
|
||||||
};
|
|
||||||
use bcrypt::verify;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
claims::Claims,
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{Login, LoginResponse},
|
|
||||||
utils::{check_email_verified, check_registration_application, check_user_valid},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn login(
|
|
||||||
data: Json<Login>,
|
|
||||||
req: HttpRequest,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> 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?;
|
|
||||||
|
|
||||||
// Verify the password
|
|
||||||
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_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,
|
|
||||||
&context.settings().hostname,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?;
|
|
||||||
|
|
||||||
Ok(Json(LoginResponse {
|
|
||||||
jwt: Some(jwt.clone()),
|
|
||||||
verify_email_sent: false,
|
|
||||||
registration_created: false,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
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, SuccessResponse};
|
|
||||||
use lemmy_db_schema::source::login_token::LoginToken;
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn logout(
|
|
||||||
req: HttpRequest,
|
|
||||||
// require login
|
|
||||||
_local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<HttpResponse> {
|
|
||||||
let jwt = read_auth_token(&req)?.ok_or(LemmyErrorType::NotLoggedIn)?;
|
|
||||||
LoginToken::invalidate(&mut context.pool(), &jwt).await?;
|
|
||||||
|
|
||||||
let mut res = HttpResponse::Ok().json(SuccessResponse::default());
|
|
||||||
let cookie = Cookie::new(AUTH_COOKIE_NAME, "");
|
|
||||||
res.add_removal_cookie(&cookie)?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
pub mod add_admin;
|
|
||||||
pub mod ban_person;
|
|
||||||
pub mod block;
|
|
||||||
pub mod change_password;
|
|
||||||
pub mod change_password_after_reset;
|
|
||||||
pub mod generate_totp_secret;
|
|
||||||
pub mod get_captcha;
|
|
||||||
pub mod list_banned;
|
|
||||||
pub mod list_logins;
|
|
||||||
pub mod list_media;
|
|
||||||
pub mod list_saved;
|
|
||||||
pub mod login;
|
|
||||||
pub mod logout;
|
|
||||||
pub mod notifications;
|
|
||||||
pub mod report_count;
|
|
||||||
pub mod reset_password;
|
|
||||||
pub mod save_settings;
|
|
||||||
pub mod update_totp;
|
|
||||||
pub mod user_block_instance;
|
|
||||||
pub mod validate_auth;
|
|
||||||
pub mod verify_email;
|
|
|
@ -1,36 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{GetPersonMentions, GetPersonMentionsResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::person_mention_view::PersonMentionQuery;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_mentions(
|
|
||||||
data: Query<GetPersonMentions>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetPersonMentionsResponse>> {
|
|
||||||
let sort = data.sort;
|
|
||||||
let page = data.page;
|
|
||||||
let limit = data.limit;
|
|
||||||
let unread_only = data.unread_only.unwrap_or_default();
|
|
||||||
let person_id = Some(local_user_view.person.id);
|
|
||||||
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
|
|
||||||
|
|
||||||
let mentions = PersonMentionQuery {
|
|
||||||
recipient_id: person_id,
|
|
||||||
my_person_id: person_id,
|
|
||||||
sort,
|
|
||||||
unread_only,
|
|
||||||
show_bot_accounts,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
.list(&mut context.pool())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(GetPersonMentionsResponse { mentions }))
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{GetReplies, GetRepliesResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_replies(
|
|
||||||
data: Query<GetReplies>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
|
||||||
let sort = data.sort;
|
|
||||||
let page = data.page;
|
|
||||||
let limit = data.limit;
|
|
||||||
let unread_only = data.unread_only.unwrap_or_default();
|
|
||||||
let person_id = Some(local_user_view.person.id);
|
|
||||||
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
|
|
||||||
|
|
||||||
let replies = CommentReplyQuery {
|
|
||||||
recipient_id: person_id,
|
|
||||||
my_person_id: person_id,
|
|
||||||
sort,
|
|
||||||
unread_only,
|
|
||||||
show_bot_accounts,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
.list(&mut context.pool())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(GetRepliesResponse { replies }))
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{context::LemmyContext, person::GetRepliesResponse};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
comment_reply::CommentReply,
|
|
||||||
person_mention::PersonMention,
|
|
||||||
private_message::PrivateMessage,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<GetRepliesResponse>> {
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
// Mark all comment_replies as read
|
|
||||||
CommentReply::mark_all_as_read(&mut context.pool(), person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
|
||||||
|
|
||||||
// Mark all user mentions as read
|
|
||||||
PersonMention::mark_all_as_read(&mut context.pool(), person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
|
||||||
|
|
||||||
// Mark all private_messages as read
|
|
||||||
PrivateMessage::mark_all_as_read(&mut context.pool(), person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
|
|
||||||
|
|
||||||
Ok(Json(GetRepliesResponse { replies: vec![] }))
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{MarkPersonMentionAsRead, PersonMentionResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::person_mention::{PersonMention, PersonMentionUpdateForm},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::PersonMentionView;
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<PersonMentionResponse>> {
|
|
||||||
let person_mention_id = data.person_mention_id;
|
|
||||||
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?;
|
|
||||||
|
|
||||||
if local_user_view.person.id != read_person_mention.recipient_id {
|
|
||||||
Err(LemmyErrorType::CouldntUpdateComment)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let person_mention_id = read_person_mention.id;
|
|
||||||
let read = Some(data.read);
|
|
||||||
PersonMention::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
person_mention_id,
|
|
||||||
&PersonMentionUpdateForm { read },
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
|
||||||
|
|
||||||
let person_mention_id = read_person_mention.id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let person_mention_view =
|
|
||||||
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?;
|
|
||||||
|
|
||||||
Ok(Json(PersonMentionResponse {
|
|
||||||
person_mention_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{CommentReplyResponse, MarkCommentReplyAsRead},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::comment_reply::{CommentReply, CommentReplyUpdateForm},
|
|
||||||
traits::Crud,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_db_views_actor::structs::CommentReplyView;
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<CommentReplyResponse>> {
|
|
||||||
let comment_reply_id = data.comment_reply_id;
|
|
||||||
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
|
|
||||||
|
|
||||||
if local_user_view.person.id != read_comment_reply.recipient_id {
|
|
||||||
Err(LemmyErrorType::CouldntUpdateComment)?
|
|
||||||
}
|
|
||||||
|
|
||||||
let comment_reply_id = read_comment_reply.id;
|
|
||||||
let read = Some(data.read);
|
|
||||||
|
|
||||||
CommentReply::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
comment_reply_id,
|
|
||||||
&CommentReplyUpdateForm { read },
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
|
||||||
|
|
||||||
let comment_reply_id = read_comment_reply.id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let comment_reply_view =
|
|
||||||
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
|
|
||||||
|
|
||||||
Ok(Json(CommentReplyResponse { comment_reply_view }))
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
pub mod list_mentions;
|
|
||||||
pub mod list_replies;
|
|
||||||
pub mod mark_all_read;
|
|
||||||
pub mod mark_mention_read;
|
|
||||||
pub mod mark_reply_read;
|
|
||||||
pub mod unread_count;
|
|
|
@ -1,29 +0,0 @@
|
||||||
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::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn unread_count(
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetUnreadCountResponse>> {
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
let replies =
|
|
||||||
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).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?;
|
|
||||||
|
|
||||||
Ok(Json(GetUnreadCountResponse {
|
|
||||||
replies,
|
|
||||||
mentions,
|
|
||||||
private_messages,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{GetReportCount, GetReportCountResponse},
|
|
||||||
utils::check_community_mod_of_any_or_admin_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, ReportCombinedViewInternal};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn report_count(
|
|
||||||
data: Query<GetReportCount>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<GetReportCountResponse>> {
|
|
||||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
|
||||||
|
|
||||||
let count = ReportCombinedViewInternal::get_report_count(
|
|
||||||
&mut context.pool(),
|
|
||||||
&local_user_view,
|
|
||||||
data.community_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(GetReportCountResponse { count }))
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::PasswordReset,
|
|
||||||
utils::{check_email_verified, send_password_reset_email},
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn reset_password(
|
|
||||||
data: Json<PasswordReset>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
let email = data.email.to_lowercase();
|
|
||||||
// For security, errors are not returned.
|
|
||||||
// https://github.com/LemmyNet/lemmy/issues/5277
|
|
||||||
let _ = try_reset_password(&email, &context).await;
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn try_reset_password(email: &str, context: &LemmyContext) -> LemmyResult<()> {
|
|
||||||
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), email).await?;
|
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
check_email_verified(&local_user_view, &site_view)?;
|
|
||||||
if let Err(e) =
|
|
||||||
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await
|
|
||||||
{
|
|
||||||
error!("Failed to send password reset email: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::SaveUserSettings,
|
|
||||||
utils::{
|
|
||||||
get_url_blocklist,
|
|
||||||
local_site_to_slur_regex,
|
|
||||||
process_markdown_opt,
|
|
||||||
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_string_update,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::{
|
|
||||||
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,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
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 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_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.deref() != email {
|
|
||||||
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
|
|
||||||
send_verification_email(
|
|
||||||
&local_user_view,
|
|
||||||
email,
|
|
||||||
&mut context.pool(),
|
|
||||||
context.settings(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(bio)) = &bio {
|
|
||||||
is_valid_bio_field(bio)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(display_name)) = &display_name {
|
|
||||||
is_valid_display_name(
|
|
||||||
display_name.trim(),
|
|
||||||
site_view.local_site.actor_name_max_length as usize,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(matrix_user_id)) = &matrix_user_id {
|
|
||||||
is_valid_matrix_id(matrix_user_id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_post_sort_type = data.default_post_sort_type;
|
|
||||||
let default_comment_sort_type = data.default_comment_sort_type;
|
|
||||||
|
|
||||||
let person_form = PersonUpdateForm {
|
|
||||||
display_name,
|
|
||||||
bio,
|
|
||||||
matrix_user_id,
|
|
||||||
bot_account: data.bot_account,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ignore errors, because 'no fields updated' will return an error.
|
|
||||||
// https://github.com/LemmyNet/lemmy/issues/4076
|
|
||||||
Person::update(&mut context.pool(), person_id, &person_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
if let Some(discussion_languages) = data.discussion_languages.clone() {
|
|
||||||
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_user_form = LocalUserUpdateForm {
|
|
||||||
email,
|
|
||||||
show_avatars: data.show_avatars,
|
|
||||||
show_read_posts: data.show_read_posts,
|
|
||||||
send_notifications_to_email: data.send_notifications_to_email,
|
|
||||||
show_nsfw: data.show_nsfw,
|
|
||||||
blur_nsfw: data.blur_nsfw,
|
|
||||||
show_bot_accounts: data.show_bot_accounts,
|
|
||||||
default_post_sort_type,
|
|
||||||
default_comment_sort_type,
|
|
||||||
default_listing_type,
|
|
||||||
theme: data.theme.clone(),
|
|
||||||
interface_language: data.interface_language.clone(),
|
|
||||||
open_links_in_new_tab: data.open_links_in_new_tab,
|
|
||||||
infinite_scroll_enabled: data.infinite_scroll_enabled,
|
|
||||||
post_listing_mode: data.post_listing_mode,
|
|
||||||
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,
|
|
||||||
auto_mark_fetched_posts_as_read: data.auto_mark_fetched_posts_as_read,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
use crate::check_totp_2fa_valid;
|
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{UpdateTotp, UpdateTotpResponse},
|
|
||||||
};
|
|
||||||
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].
|
|
||||||
///
|
|
||||||
/// To enable, you need to first call [generate_totp_secret] and then pass a valid token to this
|
|
||||||
/// function.
|
|
||||||
///
|
|
||||||
/// Disabling is only possible if 2FA was previously enabled. Again it is necessary to pass a valid
|
|
||||||
/// token.
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn update_totp(
|
|
||||||
data: Json<UpdateTotp>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<UpdateTotpResponse>> {
|
|
||||||
check_totp_2fa_valid(
|
|
||||||
&local_user_view,
|
|
||||||
&Some(data.totp_token.clone()),
|
|
||||||
&context.settings().hostname,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// toggle the 2fa setting
|
|
||||||
let local_user_form = LocalUserUpdateForm {
|
|
||||||
totp_2fa_enabled: Some(data.enabled),
|
|
||||||
// if totp is enabled, leave unchanged. otherwise clear secret
|
|
||||||
totp_2fa_secret: if data.enabled { None } else { Some(None) },
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
LocalUser::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
local_user_view.local_user.id,
|
|
||||||
&local_user_form,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(UpdateTotpResponse {
|
|
||||||
enabled: data.enabled,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::instance_block::{InstanceBlock, InstanceBlockForm},
|
|
||||||
traits::Blockable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn user_block_instance(
|
|
||||||
data: Json<UserBlockInstanceParams>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
if data.block {
|
|
||||||
InstanceBlock::block(&mut context.pool(), &instance_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
|
|
||||||
} else {
|
|
||||||
InstanceBlock::unblock(&mut context.pool(), &instance_block_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
use crate::{local_user_view_from_jwt, read_auth_token};
|
|
||||||
use actix_web::{
|
|
||||||
web::{Data, Json},
|
|
||||||
HttpRequest,
|
|
||||||
};
|
|
||||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
|
||||||
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.
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn validate_auth(
|
|
||||||
req: HttpRequest,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
let jwt = read_auth_token(&req)?;
|
|
||||||
if let Some(jwt) = jwt {
|
|
||||||
local_user_view_from_jwt(&jwt, &context).await?;
|
|
||||||
} else {
|
|
||||||
Err(LemmyErrorType::NotLoggedIn)?;
|
|
||||||
}
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::VerifyEmail,
|
|
||||||
utils::send_new_applicant_email_to_admins,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::{
|
|
||||||
email_verification::EmailVerification,
|
|
||||||
local_user::{LocalUser, LocalUserUpdateForm},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
pub async fn verify_email(
|
|
||||||
data: Json<VerifyEmail>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> 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?;
|
|
||||||
let local_user_id = verification.local_user_id;
|
|
||||||
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
|
|
||||||
|
|
||||||
// Check if their email has already been verified once, before this
|
|
||||||
let email_already_verified = local_user_view.local_user.email_verified;
|
|
||||||
|
|
||||||
let form = LocalUserUpdateForm {
|
|
||||||
// necessary in case this is a new signup
|
|
||||||
email_verified: Some(true),
|
|
||||||
// necessary in case email of an existing user was changed
|
|
||||||
email: Some(Some(verification.email)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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, and the user hasn't
|
|
||||||
// already been verified.
|
|
||||||
if site_view.local_site.application_email_admins && !email_already_verified {
|
|
||||||
send_new_applicant_email_to_admins(
|
|
||||||
&local_user_view.person.name,
|
|
||||||
&mut context.pool(),
|
|
||||||
context.settings(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,75 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
build_response::build_post_response,
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{FeaturePost, PostResponse},
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{check_community_mod_action, is_admin},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::Community,
|
|
||||||
mod_log::moderator::{ModFeaturePost, ModFeaturePostForm},
|
|
||||||
post::{Post, PostUpdateForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
PostFeatureType,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn feature_post(
|
|
||||||
data: Json<FeaturePost>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<PostResponse>> {
|
|
||||||
let post_id = data.post_id;
|
|
||||||
let orig_post = Post::read(&mut context.pool(), post_id).await?;
|
|
||||||
|
|
||||||
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
&community,
|
|
||||||
false,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if data.feature_type == PostFeatureType::Local {
|
|
||||||
is_admin(&local_user_view)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the post
|
|
||||||
let post_id = data.post_id;
|
|
||||||
let new_post: PostUpdateForm = if data.feature_type == PostFeatureType::Community {
|
|
||||||
PostUpdateForm {
|
|
||||||
featured_community: Some(data.featured),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PostUpdateForm {
|
|
||||||
featured_local: Some(data.featured),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let post = Post::update(&mut context.pool(), post_id, &new_post).await?;
|
|
||||||
|
|
||||||
// Mod tables
|
|
||||||
let form = ModFeaturePostForm {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
post_id: data.post_id,
|
|
||||||
featured: Some(data.featured),
|
|
||||||
is_featured_community: Some(data.feature_type == PostFeatureType::Community),
|
|
||||||
};
|
|
||||||
|
|
||||||
ModFeaturePost::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured),
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
|
||||||
request::fetch_link_metadata,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn get_link_metadata(
|
|
||||||
data: Query<GetSiteMetadata>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
// 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 }))
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{HidePost, PostResponse},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::source::post::PostHide;
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, PostView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn hide_post(
|
|
||||||
data: Json<HidePost>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<PostResponse>> {
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let post_id = data.post_id;
|
|
||||||
|
|
||||||
// Mark the post as hidden / unhidden
|
|
||||||
if data.hide {
|
|
||||||
PostHide::hide(&mut context.pool(), post_id, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
|
||||||
} else {
|
|
||||||
PostHide::unhide(&mut context.pool(), post_id, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let post_view = PostView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
post_id,
|
|
||||||
Some(&local_user_view.local_user),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(PostResponse { post_view }))
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
build_response::build_post_response,
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{CreatePostLike, PostResponse},
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
local_site::LocalSite,
|
|
||||||
post::{PostLike, PostLikeForm, PostRead, PostReadForm},
|
|
||||||
},
|
|
||||||
traits::Likeable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, PostView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn like_post(
|
|
||||||
data: Json<CreatePostLike>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<PostResponse>> {
|
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
|
||||||
let post_id = data.post_id;
|
|
||||||
|
|
||||||
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 = PostView::read(&mut context.pool(), post_id, None, false).await?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
&post.community,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
|
|
||||||
|
|
||||||
// Remove any likes first
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
|
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
|
||||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
|
||||||
if do_add {
|
|
||||||
PostLike::like(&mut context.pool(), &like_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark Post Read
|
|
||||||
let read_form = PostReadForm::new(post_id, person_id);
|
|
||||||
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::LikePostOrComment {
|
|
||||||
object_id: post.post.ap_id,
|
|
||||||
actor: local_user_view.person.clone(),
|
|
||||||
community: post.community.clone(),
|
|
||||||
score: data.score,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
build_post_response(context.deref(), post.community.id, local_user_view, post_id).await
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{ListPostLikes, ListPostLikesResponse},
|
|
||||||
utils::is_mod_or_admin,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{source::post::Post, traits::Crud};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, VoteView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
/// 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 }))
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue