Compare commits
16 Commits
main
...
remove-log
Author | SHA1 | Date |
---|---|---|
Felix Ableitner | b02d67fd85 | |
Felix Ableitner | 1790d41855 | |
Felix Ableitner | a90adb8582 | |
Felix Ableitner | ac73470a17 | |
Felix Ableitner | 429b7f6e5c | |
Felix Ableitner | 32f69775c3 | |
Felix Ableitner | 32c4d3224c | |
Felix Ableitner | 7649dd048b | |
Felix Ableitner | af1204c0d0 | |
Felix Ableitner | c5f08768cd | |
Felix Ableitner | 89f9ecb634 | |
Felix Ableitner | 6cf4e39406 | |
Felix Ableitner | 9629f8140b | |
Felix Ableitner | dbf9c69709 | |
Felix Ableitner | 9cbb80ca8e | |
Felix Ableitner | 2bdb15b06d |
|
@ -4,5 +4,5 @@ docker
|
||||||
api_tests
|
api_tests
|
||||||
ansible
|
ansible
|
||||||
tests
|
tests
|
||||||
|
.git
|
||||||
*.sh
|
*.sh
|
||||||
pictrs
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Normalize EOL for all files that Git considers text files.
|
|
||||||
* text=auto eol=lf
|
|
|
@ -1,3 +0,0 @@
|
||||||
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917
|
|
||||||
crates/apub/ @Nutomic
|
|
||||||
migrations/ @dessalines @phiresky @dullbananas
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
name: "\U0001F41E Bug Report"
|
||||||
|
about: Create a report to help us improve Lemmy
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Found a bug? Please fill out the sections below. 👍
|
||||||
|
|
||||||
|
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
|
||||||
|
|
||||||
|
### Issue Summary
|
||||||
|
|
||||||
|
A summary of the bug.
|
||||||
|
|
||||||
|
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
1. (for example) I clicked login, and an endless spinner show up.
|
||||||
|
2. I tried to install lemmy via this guide, and I'm getting this error.
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
### Technical details
|
||||||
|
|
||||||
|
* Please post your log: `sudo docker-compose logs > lemmy_log.out`.
|
||||||
|
* What OS are you trying to install lemmy on?
|
||||||
|
* Any browser console errors?
|
|
@ -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
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
name: "\U0001F680 Feature request"
|
||||||
|
about: Suggest an idea for improving Lemmy
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
|
||||||
|
|
||||||
|
### Is your proposal related to a problem?
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Provide a clear and concise description of what the problem is.
|
||||||
|
For example, "I'm always frustrated when..."
|
||||||
|
-->
|
||||||
|
|
||||||
|
(Write your answer here.)
|
||||||
|
|
||||||
|
### Describe the solution you'd like
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Provide a clear and concise description of what you want to happen.
|
||||||
|
-->
|
||||||
|
|
||||||
|
(Describe your proposed solution here.)
|
||||||
|
|
||||||
|
### Describe alternatives you've considered
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Let us know about other solutions you've tried or researched.
|
||||||
|
-->
|
||||||
|
|
||||||
|
(Write your answer here.)
|
||||||
|
|
||||||
|
### Additional context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Is there anything else you can add about the proposal?
|
||||||
|
You might want to link to related issues here, if you haven't already.
|
||||||
|
-->
|
||||||
|
|
||||||
|
(Write your answer here.)
|
|
@ -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.
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: "? Question"
|
||||||
|
about: General questions about Lemmy
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
What's the question you have about lemmy?
|
|
@ -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
|
|
|
@ -1,5 +1,6 @@
|
||||||
# local ansible configuration
|
# local ansible configuration
|
||||||
ansible/inventory
|
ansible/inventory
|
||||||
|
ansible/inventory_dev
|
||||||
ansible/passwords/
|
ansible/passwords/
|
||||||
|
|
||||||
# docker build files
|
# docker build files
|
||||||
|
@ -14,23 +15,8 @@ volumes
|
||||||
# local build files
|
# local build files
|
||||||
target
|
target
|
||||||
env_setup.sh
|
env_setup.sh
|
||||||
query_testing/**/reports/*.json
|
query_testing/*.json
|
||||||
|
query_testing/*.json.old
|
||||||
|
|
||||||
# API tests
|
# API tests
|
||||||
api_tests/node_modules
|
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
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
[submodule "crates/utils/translations"]
|
|
||||||
path = crates/utils/translations
|
|
||||||
url = https://github.com/LemmyNet/lemmy-translations.git
|
|
||||||
branch = main
|
|
|
@ -1,7 +1,5 @@
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
edition = "2021"
|
edition="2018"
|
||||||
imports_layout = "HorizontalVertical"
|
imports_layout="HorizontalVertical"
|
||||||
imports_granularity = "Crate"
|
merge_imports=true
|
||||||
group_imports = "One"
|
reorder_imports=true
|
||||||
wrap_comments = true
|
|
||||||
comment_width = 100
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
sudo: required
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 14
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
env:
|
||||||
|
matrix:
|
||||||
|
- DOCKER_COMPOSE_VERSION=1.25.5
|
||||||
|
global:
|
||||||
|
- secure: nzmFoTxPn7OT+qcTULezSCT6B44j/q8RxERBQSr1FVXaCcDrBr6q9ewhGy7BHWP74r4qbif4m9r3sNELZCoFYFP3JwLnrZfX/xUwU8p61eFD2PMOJAdOywDxb94SvooOSnjBmxNvRsuqf6Zmnw378mbsSVCi9Xbx9jpoV4Jq8zKgO0M8WIl/lj2dijD95WIMrHcorbzKS3+2zW3LkPiC2bnfDAUmUDfaCj1gh9FCvzZMtrSxu7kxAeFCkR16TJUciIcGgag8rLHfxwG0h2uEJJ+3/62qCWUdgnj171oTE4ZRi0hdvt2HOY5wjHfS2y1ZxWYgo31uws3pyoTNeQZi0o7Q9Xe/4JXYZXvDfuscSZ9RiuhAstCVswtXPJJVVJQ9cdl5eX1TI0bz8eVRvRy4p40OIBjKiobkmRjl8sXjFbpYAIvFr+TgSa/K/bxm3POfI0B8bIHI85zFxUMrWt5i2IJ0dWvDNHrz+CWWKn1vVFYbBNPgDDHtE0P3LWLEioWFf+ULycjW8DefWc+b63Lf9SSaEE7FnX2mc+BaHCgubCDkJy9Au4xP8zQlJjgZwOdTedw5jvmwz3fqMZBpHypVUXzZs7cRhMWtQ7TAoGb8TOqXNgPEVW+BARNXl0wAamTgjt9v20x0wkp+/SLJwMNY+zvwmzxzd5R9TPgDOqyIRTU=
|
||||||
|
- secure: ALZqC4OYV315P7EZyk+c/PLJdneeU7jMC30TTzMcX3hospIu7naWekZ+HUnziFDQKZxIHWKZsq1R52DWhsERLrPF3SVa+QiXu8vTTPrETBWnu9VgyFzgdEbUKRas1X3qerEAHcNBms1EAl2FOiQM1k5EDygrClv4KWgyzntEtKJbN2UCFKxtoBSdMZA6fcGtCwffcj8uIAIP2NhZixbU+smVgVbpMpe6QEuuEoVlVrfH8iXxb8Gi+qkd0YIYAHkjtTqQ/nHuAUhcuEE0mORTNGPv7CmTwpuQiGCCdtySZc7Qq8z1x2y7RLy0+RVxM0PR8UV6iy4ipyTgZ6wTF30ksLDxOI3GlRaKF3F6kLErOiEiEUOqa+zLgUM0OLGTn+KLATQDx74in5NcKjKUAnkuxdZyuDbifvQb5tqfrGdXd22pzVZbielRJRW59ig0Nr5cxEpRtoRkoFKNk7o3XlD6JmIBjKn1UHkZ4H/oLUKIXT2qOP2fIEzgLjfpSuGwhvJRz1KRP49HYVl7Gkd45/RdZ519W0gnMkIrEaod90iXSFNTgmJTGeH0Mv0jHameN47PIT3c49MOy5Hj0XCHUPfc6qqrdGnliS5hTnrFThCfn5ZuSZxVdgGLJUQvV+D+5KDqjFdGyNGVGoEg0YdrDtGXmpojbyQDJAT7ToL3yIBF7co=
|
||||||
|
before_install:
|
||||||
|
# Install docker-compose
|
||||||
|
- sudo rm /usr/local/bin/docker-compose
|
||||||
|
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname
|
||||||
|
-s`-`uname -m` > docker-compose
|
||||||
|
- chmod +x docker-compose
|
||||||
|
- sudo mv docker-compose /usr/local/bin
|
||||||
|
# Change dir
|
||||||
|
- cd docker/travis
|
||||||
|
script:
|
||||||
|
- "./run-tests.bash"
|
||||||
|
deploy:
|
||||||
|
provider: script
|
||||||
|
script: bash docker_push.sh
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
notifications:
|
||||||
|
email: false
|
312
.woodpecker.yml
312
.woodpecker.yml
|
@ -1,312 +0,0 @@
|
||||||
# TODO: The when: platform conditionals aren't working currently
|
|
||||||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
|
||||||
|
|
||||||
variables:
|
|
||||||
- &rust_image "rust:1.77"
|
|
||||||
- &rust_nightly_image "rustlang/rust:nightly"
|
|
||||||
- &install_pnpm "corepack enable pnpm"
|
|
||||||
- &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",
|
|
||||||
]
|
|
||||||
- install_binstall: &install_binstall
|
|
||||||
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
- cp cargo-binstall /usr/local/cargo/bin
|
|
||||||
- install_diesel_cli: &install_diesel_cli
|
|
||||||
- apt update && apt install -y lsb-release build-essential
|
|
||||||
- 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
|
|
||||||
- cargo install diesel_cli --no-default-features --features postgres
|
|
||||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
|
||||||
|
|
||||||
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.0.0
|
|
||||||
commands:
|
|
||||||
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
toml_fmt:
|
|
||||||
image: tamasfe/taplo:0.8.1
|
|
||||||
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_machete:
|
|
||||||
image: *rust_nightly_image
|
|
||||||
commands:
|
|
||||||
- <<: *install_binstall
|
|
||||||
- cargo binstall -y cargo-machete
|
|
||||||
- cargo machete
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
ignored_files:
|
|
||||||
image: alpine:3
|
|
||||||
commands:
|
|
||||||
- apk add git
|
|
||||||
- IGNORED=$(git ls-files --cached -i --exclude-standard)
|
|
||||||
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
# make sure api builds with default features (used by other crates relying on lemmy api)
|
|
||||||
check_api_common_default_features:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_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:
|
|
||||||
- export LEMMY_CONFIG_LOCATION=./config/config.hjson
|
|
||||||
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
|
|
||||||
- diff config/defaults.hjson config/defaults_current.hjson
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_diesel_schema:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
commands:
|
|
||||||
- <<: *install_diesel_cli
|
|
||||||
- diesel migration run
|
|
||||||
- diesel print-schema --config-file=diesel.toml > tmp.schema
|
|
||||||
- diff tmp.schema crates/db_schema/src/schema.rs
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_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
|
|
||||||
- export LEMMY_CONFIG_LOCATION=config/config.hjson
|
|
||||||
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
cargo_clippy:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- rustup component add clippy
|
|
||||||
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings
|
|
||||||
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
|
|
||||||
|
|
||||||
cargo_test:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
|
|
||||||
- cargo test --workspace --no-fail-fast
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_diesel_migration:
|
|
||||||
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
PGUSER: lemmy
|
|
||||||
PGPASSWORD: password
|
|
||||||
PGHOST: database
|
|
||||||
PGDATABASE: lemmy
|
|
||||||
commands:
|
|
||||||
# Install diesel_cli
|
|
||||||
- <<: *install_diesel_cli
|
|
||||||
# Run all migrations
|
|
||||||
- diesel migration run
|
|
||||||
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
|
|
||||||
- apt update && apt install -y lsb-release
|
|
||||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
|
||||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
|
||||||
- apt update && apt install -y postgresql-client-16
|
|
||||||
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
|
||||||
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
|
|
||||||
# Make sure that the newest migration is revertable without the `r` schema
|
|
||||||
- diesel migration redo
|
|
||||||
# Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things
|
|
||||||
- alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending"
|
|
||||||
- lemmy_schema_setup
|
|
||||||
- lemmy_schema_setup
|
|
||||||
# Make sure that the newest migration is revertable with the `r` schema
|
|
||||||
- diesel migration redo
|
|
||||||
# Check for changes in the schema, which would be caused by an incorrect migration
|
|
||||||
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
|
||||||
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump
|
|
||||||
- diff before.sqldump after.sqldump
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
run_federation_tests:
|
|
||||||
image: node:20-bookworm-slim
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
|
|
||||||
DO_WRITE_HOSTS_FILE: "1"
|
|
||||||
commands:
|
|
||||||
- *install_pnpm
|
|
||||||
- apt update && apt 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
|
|
||||||
secrets: [docker_username, docker_password]
|
|
||||||
settings:
|
|
||||||
repo: dessalines/lemmy
|
|
||||||
dockerfile: docker/Dockerfile
|
|
||||||
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
|
|
||||||
secrets: [docker_username, docker_password]
|
|
||||||
settings:
|
|
||||||
repo: dessalines/lemmy
|
|
||||||
dockerfile: docker/Dockerfile
|
|
||||||
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
|
|
||||||
commands:
|
|
||||||
- <<: *install_binstall
|
|
||||||
# Install cargo-workspaces
|
|
||||||
- cargo binstall -y cargo-workspaces
|
|
||||||
- cp -r migrations crates/db_schema/
|
|
||||||
- cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
|
|
||||||
secrets: [cargo_api_token]
|
|
||||||
when:
|
|
||||||
- event: tag
|
|
||||||
|
|
||||||
notify_on_failure:
|
|
||||||
image: alpine:3
|
|
||||||
commands:
|
|
||||||
- apk add curl
|
|
||||||
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
|
|
||||||
when:
|
|
||||||
- event: [pull_request, tag]
|
|
||||||
status: failure
|
|
||||||
|
|
||||||
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:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: lemmy
|
|
||||||
POSTGRES_PASSWORD: password
|
|
|
@ -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.ml/LemmyNet/lemmy](https://yerbamate.ml/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/).
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
See [here](https://lemmy.ml/docs/contributing.html) for contributing Instructions.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
244
Cargo.toml
244
Cargo.toml
|
@ -1,210 +1,56 @@
|
||||||
[workspace.package]
|
|
||||||
version = "0.19.4-rc.2"
|
|
||||||
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]
|
[package]
|
||||||
name = "lemmy_server"
|
name = "lemmy_server"
|
||||||
version.workspace = true
|
version = "0.0.1"
|
||||||
edition.workspace = true
|
edition = "2018"
|
||||||
description.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
homepage.workspace = true
|
|
||||||
documentation.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
doctest = false
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = 0
|
lto = true
|
||||||
lto = "thin"
|
|
||||||
strip = true # Automatically strip symbols from the binary.
|
|
||||||
opt-level = "z" # Optimize for size.
|
|
||||||
|
|
||||||
# This profile significantly speeds up build time. If debug info is needed you can comment the line
|
|
||||||
# out temporarily, but make sure to leave this in the main branch.
|
|
||||||
[profile.dev]
|
|
||||||
debug = 0
|
|
||||||
|
|
||||||
[features]
|
|
||||||
embed-pictrs = ["pict-rs"]
|
|
||||||
# This feature requires building with `tokio_unstable` flag, see documentation:
|
|
||||||
# https://docs.rs/tokio/latest/tokio/#unstable-features
|
|
||||||
console = [
|
|
||||||
"console-subscriber",
|
|
||||||
"opentelemetry",
|
|
||||||
"opentelemetry-otlp",
|
|
||||||
"tracing-opentelemetry",
|
|
||||||
"reqwest-tracing/opentelemetry_0_16",
|
|
||||||
]
|
|
||||||
json-log = ["tracing-subscriber/json"]
|
|
||||||
default = []
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/api",
|
"lemmy_api",
|
||||||
"crates/api_crud",
|
"lemmy_apub",
|
||||||
"crates/api_common",
|
"lemmy_utils",
|
||||||
"crates/apub",
|
"lemmy_db",
|
||||||
"crates/utils",
|
"lemmy_structs",
|
||||||
"crates/db_perf",
|
"lemmy_rate_limit",
|
||||||
"crates/db_schema",
|
"lemmy_websocket",
|
||||||
"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"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
lemmy_api = { version = "=0.19.4-rc.2", path = "./crates/api" }
|
|
||||||
lemmy_api_crud = { version = "=0.19.4-rc.2", path = "./crates/api_crud" }
|
|
||||||
lemmy_apub = { version = "=0.19.4-rc.2", path = "./crates/apub" }
|
|
||||||
lemmy_utils = { version = "=0.19.4-rc.2", path = "./crates/utils", default-features = false }
|
|
||||||
lemmy_db_schema = { version = "=0.19.4-rc.2", path = "./crates/db_schema" }
|
|
||||||
lemmy_api_common = { version = "=0.19.4-rc.2", path = "./crates/api_common" }
|
|
||||||
lemmy_routes = { version = "=0.19.4-rc.2", path = "./crates/routes" }
|
|
||||||
lemmy_db_views = { version = "=0.19.4-rc.2", path = "./crates/db_views" }
|
|
||||||
lemmy_db_views_actor = { version = "=0.19.4-rc.2", path = "./crates/db_views_actor" }
|
|
||||||
lemmy_db_views_moderator = { version = "=0.19.4-rc.2", path = "./crates/db_views_moderator" }
|
|
||||||
lemmy_federate = { version = "=0.19.4-rc.2", path = "./crates/federate" }
|
|
||||||
activitypub_federation = { version = "0.5.6", default-features = false, features = [
|
|
||||||
"actix-web",
|
|
||||||
] }
|
|
||||||
diesel = "2.1.6"
|
|
||||||
diesel_migrations = "2.1.0"
|
|
||||||
diesel-async = "0.4.1"
|
|
||||||
serde = { version = "1.0.202", features = ["derive"] }
|
|
||||||
serde_with = "3.8.1"
|
|
||||||
actix-web = { version = "4.6.0", default-features = false, features = [
|
|
||||||
"macros",
|
|
||||||
"rustls",
|
|
||||||
"compress-brotli",
|
|
||||||
"compress-gzip",
|
|
||||||
"compress-zstd",
|
|
||||||
"cookies",
|
|
||||||
] }
|
|
||||||
tracing = "0.1.40"
|
|
||||||
tracing-actix-web = { version = "0.7.10", default-features = false }
|
|
||||||
tracing-error = "0.2.0"
|
|
||||||
tracing-log = "0.2.0"
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
|
||||||
url = { version = "2.5.0", features = ["serde"] }
|
|
||||||
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
|
|
||||||
reqwest-middleware = "0.2.5"
|
|
||||||
reqwest-tracing = "0.4.8"
|
|
||||||
clokwerk = "0.4.0"
|
|
||||||
doku = { version = "0.21.1", features = ["url-2"] }
|
|
||||||
bcrypt = "0.15.1"
|
|
||||||
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
|
||||||
serde_json = { version = "1.0.117", features = ["preserve_order"] }
|
|
||||||
base64 = "0.22.1"
|
|
||||||
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
|
||||||
async-trait = "0.1.80"
|
|
||||||
captcha = "0.0.9"
|
|
||||||
anyhow = { version = "1.0.86", features = [
|
|
||||||
"backtrace",
|
|
||||||
] } # backtrace is on by default on nightly, but not stable rust
|
|
||||||
diesel_ltree = "0.3.1"
|
|
||||||
typed-builder = "0.18.2"
|
|
||||||
serial_test = "3.1.1"
|
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
|
||||||
regex = "1.10.4"
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
diesel-derive-newtype = "2.1.2"
|
|
||||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
|
||||||
strum = "0.26.2"
|
|
||||||
strum_macros = "0.26.2"
|
|
||||||
itertools = "0.13.0"
|
|
||||||
futures = "0.3.30"
|
|
||||||
http = "0.2.12"
|
|
||||||
rosetta-i18n = "0.1.3"
|
|
||||||
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
|
|
||||||
tracing-opentelemetry = { version = "0.19.0" }
|
|
||||||
ts-rs = { version = "7.1.1", features = [
|
|
||||||
"serde-compat",
|
|
||||||
"chrono-impl",
|
|
||||||
"no-serde-warnings",
|
|
||||||
] }
|
|
||||||
rustls = { version = "0.23.8", features = ["ring"] }
|
|
||||||
futures-util = "0.3.30"
|
|
||||||
tokio-postgres = "0.7.10"
|
|
||||||
tokio-postgres-rustls = "0.12.0"
|
|
||||||
urlencoding = "2.1.3"
|
|
||||||
enum-map = "2.7"
|
|
||||||
moka = { version = "0.12.7", features = ["future"] }
|
|
||||||
i-love-jesus = { version = "0.1.0" }
|
|
||||||
clap = { version = "4.5.4", features = ["derive", "env"] }
|
|
||||||
pretty_assertions = "1.4.0"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lemmy_api = { workspace = true }
|
lemmy_api = { path = "./lemmy_api" }
|
||||||
lemmy_api_crud = { workspace = true }
|
lemmy_apub = { path = "./lemmy_apub" }
|
||||||
lemmy_apub = { workspace = true }
|
lemmy_utils = { path = "./lemmy_utils" }
|
||||||
lemmy_utils = { workspace = true }
|
lemmy_db = { path = "./lemmy_db" }
|
||||||
lemmy_db_schema = { workspace = true }
|
lemmy_structs = { path = "./lemmy_structs" }
|
||||||
lemmy_api_common = { workspace = true }
|
lemmy_rate_limit = { path = "./lemmy_rate_limit" }
|
||||||
lemmy_routes = { workspace = true }
|
lemmy_websocket = { path = "./lemmy_websocket" }
|
||||||
lemmy_federate = { workspace = true }
|
diesel = "1.4"
|
||||||
activitypub_federation = { workspace = true }
|
diesel_migrations = "1.4"
|
||||||
diesel = { workspace = true }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
diesel-async = { workspace = true }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
actix-web = { workspace = true }
|
actix = "0.10"
|
||||||
tracing = { workspace = true }
|
actix-web = { version = "3.1", default-features = false, features = ["rustls"] }
|
||||||
tracing-actix-web = { workspace = true }
|
actix-files = { version = "0.4", default-features = false }
|
||||||
tracing-error = { workspace = true }
|
actix-web-actors = { version = "3.0", default-features = false }
|
||||||
tracing-log = { workspace = true }
|
awc = { version = "2.0", default-features = false }
|
||||||
tracing-subscriber = { workspace = true }
|
log = "0.4"
|
||||||
url = { workspace = true }
|
env_logger = "0.8"
|
||||||
reqwest = { workspace = true }
|
strum = "0.19"
|
||||||
reqwest-middleware = { workspace = true }
|
lazy_static = "1.3"
|
||||||
reqwest-tracing = { workspace = true }
|
rss = "1.9"
|
||||||
clokwerk = { workspace = true }
|
url = { version = "2.1", features = ["serde"] }
|
||||||
serde_json = { workspace = true }
|
openssl = "0.10"
|
||||||
tracing-opentelemetry = { workspace = true, optional = true }
|
http-signature-normalization-actix = { version = "0.4", default-features = false, features = ["sha-2"] }
|
||||||
opentelemetry = { workspace = true, optional = true }
|
tokio = "0.3"
|
||||||
console-subscriber = { version = "0.1.10", optional = true }
|
sha2 = "0.9"
|
||||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
anyhow = "1.0"
|
||||||
pict-rs = { version = "0.5.14", optional = true }
|
reqwest = { version = "0.10", features = ["json"] }
|
||||||
tokio.workspace = true
|
activitystreams = "0.7.0-alpha.4"
|
||||||
actix-cors = "0.7.0"
|
actix-rt = { version = "1.1", default-features = false }
|
||||||
futures-util = { workspace = true }
|
serde_json = { version = "1.0", features = ["preserve_order"]}
|
||||||
chrono = { workspace = true }
|
|
||||||
prometheus = { version = "0.13.4", features = ["process"] }
|
|
||||||
serial_test = { workspace = true }
|
|
||||||
clap = { workspace = true }
|
|
||||||
actix-web-prom = "0.8.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies.cargo-husky]
|
||||||
pretty_assertions = { workspace = true }
|
version = "1"
|
||||||
|
default-features = false # Disable features which are enabled by default
|
||||||
|
features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"]
|
||||||
|
|
87
README.md
87
README.md
|
@ -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=main)](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.ml/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.ml/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://join.lemmy.ml/" rel="noopener">
|
||||||
<a href="readmes/README.es.md">Español</a> |
|
|
||||||
<a href="readmes/README.ru.md">Русский</a> |
|
|
||||||
<a href="readmes/README.zh.hans.md">汉语</a> |
|
|
||||||
<a href="readmes/README.zh.hant.md">漢語</a> |
|
|
||||||
<a href="readmes/README.ja.md">日本語</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://join-lemmy.org/" rel="noopener">
|
|
||||||
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmyNet/lemmy-ui/main/src/assets/icons/favicon.svg"></a>
|
<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>
|
<h3 align="center"><a href="https://join.lemmy.ml">Lemmy</a></h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A link aggregator and forum for the fediverse.
|
A link aggregator / Reddit clone for the fediverse.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a href="https://join-lemmy.org">Join Lemmy</a>
|
<a href="https://join.lemmy.ml">Join Lemmy</a>
|
||||||
·
|
·
|
||||||
<a href="https://join-lemmy.org/docs/index.html">Documentation</a>
|
<a href="https://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/main/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/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/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), 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,7 +65,7 @@ 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](https://lemmy.ml/docs/administration_install_docker.html) and [Ansible](https://lemmy.ml/docs/administration_install_ansible.html).
|
||||||
- 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.
|
||||||
|
@ -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,39 @@ 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://lemmy.ml/docs/administration_install_docker.html)
|
||||||
|
- [Ansible](https://lemmy.ml/docs/administration_install_ansible.html)
|
||||||
|
|
||||||
## Lemmy Projects
|
## Lemmy Projects
|
||||||
|
|
||||||
- [awesome-lemmy - A community driven list of apps and tools for lemmy](https://github.com/dbeley/awesome-lemmy)
|
### Apps
|
||||||
|
|
||||||
|
- [lemmy-ui - The official web app for lemmy](https://github.com/LemmyNet/lemmy-ui)
|
||||||
|
- [Lemmur - A flutter lemmy app ( under development )](https://github.com/krawieck/lemmur)
|
||||||
|
- [Lemmy-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
- [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client)
|
||||||
|
- [Kotlin API ( under development )](https://github.com/eiknat/lemmy-client)
|
||||||
|
- [Dart API client ( under development )](https://github.com/krawieck/lemmy_api_client)
|
||||||
|
|
||||||
## 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://join.lemmy.ml/sponsors).
|
||||||
|
|
||||||
### Crypto
|
### Crypto
|
||||||
|
|
||||||
|
@ -136,28 +134,23 @@ 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://lemmy.ml/docs/contributing.html)
|
||||||
|
- [Docker Development](https://lemmy.ml/docs/contributing_docker_development.html)
|
||||||
- [Contributing instructions](https://join-lemmy.org/docs/contributors/01-overview.html)
|
- [Local Development](https://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.ml/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://matrix.to/#/#lemmy: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.ml/LemmyNet/lemmy)
|
||||||
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
|
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
207
RELEASES.md
207
RELEASES.md
|
@ -1,3 +1,206 @@
|
||||||
[Lemmy Releases / news](https://join-lemmy.org/news)
|
# Lemmy v0.8.0 Release (2020-10-16)
|
||||||
|
|
||||||
[Github link](https://github.com/LemmyNet/joinlemmy-site/tree/main/src/assets/news)
|
## Changes
|
||||||
|
|
||||||
|
We've been working at warp speed since our `v0.7.0` release in June, adding over [870 commits](https://github.com/LemmyNet/lemmy/compare/v0.7.0...main) since then. :sweat:
|
||||||
|
|
||||||
|
Here are some of the bigger changes:
|
||||||
|
|
||||||
|
### LemmyNet projects
|
||||||
|
|
||||||
|
- Created [LemmyNet](https://github.com/LemmyNet), where all lemmy-related projects live.
|
||||||
|
- Split out the frontend into a separete repository, [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
|
||||||
|
- Created a [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client), for any js / typescript developers.
|
||||||
|
- Split out i18n [lemmy-translations](https://github.com/LemmyNet/lemmy-translations), that any app or site developers can import and use. Lemmy currently supports [~30 languages!](https://weblate.yerbamate.ml/projects/lemmy/lemmy/)
|
||||||
|
|
||||||
|
### Lemmy Server
|
||||||
|
|
||||||
|
#### Federation
|
||||||
|
|
||||||
|
- The first **federation public beta release**, woohoo :fireworks:
|
||||||
|
- All Lemmy functionality now works over ActivityPub (except turning remote users into mods/admins)
|
||||||
|
- Instance allowlist and blocklist
|
||||||
|
- Documentation for [admins](https://lemmy.ml/docs/administration_federation.html) and [devs](https://lemmy.ml/docs/contributing_federation_overview.html) on how federation works
|
||||||
|
- Upgraded to newest versions of @asonix activitypub libraries
|
||||||
|
- Full local federation setup for manual testing
|
||||||
|
- Automated testing for nearly every federation action
|
||||||
|
- Many additional security checks
|
||||||
|
- Lots and lots of refactoring
|
||||||
|
- Asynchronous sending of outgoing activities
|
||||||
|
|
||||||
|
### User Interface
|
||||||
|
|
||||||
|
- Separated the UI from the server code, in [lemmy-ui](https://github.com/LemmyNet/lemmy-ui).
|
||||||
|
- The UI can now read with javascript disabled!
|
||||||
|
- It's now a fully isomorphic application using [inferno-isomorphic](https://infernojs.org/docs/guides/isomorphic). This means that page loads are now much faster, as the server does the work.
|
||||||
|
- The UI now also supports open-graph and twitter cards! Linking to lemmy posts (from whatever platform you use) looks pretty now: ![](https://i.imgur.com/6TZ2v7s.png)
|
||||||
|
- Improved the search page ( more features incoming ).
|
||||||
|
- The default view is now `Local`, instead of `All`, since all would show all federated posts.
|
||||||
|
- User settings are now shared across browsers ( a page refresh will pick up changes ).
|
||||||
|
- A much leaner mobile view.
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
- Re-organized the rust codebase into separate workspaces for backend and frontend.
|
||||||
|
- Removed materialized views, making the database **a lot faster**.
|
||||||
|
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
|
||||||
|
- New sort for `Local` ( meaning from local communities).
|
||||||
|
- Customizeable site, user, and community icons and banners.
|
||||||
|
- Added user preferred names / display names, bios, and cakedays.
|
||||||
|
- Visual / Audio captchas through the lemmy API.
|
||||||
|
- Lots of API field verifications.
|
||||||
|
- Upgraded to pictrs-v2 ( thanks to @asonix )
|
||||||
|
- Wayyy too many bugfixes to count.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
We'd also like to thank both the [NLnet foundation](https://nlnet.nl/) for their support in allowing us to work full-time on Lemmy ( as well as their support for [other important open-source projects](https://nlnet.nl/project/current.html) ), [those who sponsor us](https://lemmy.ml/sponsors), and those who [help translate Lemmy](https://weblate.yerbamate.ml/projects/lemmy/). Every little bit does help. We remain committed to never allowing advertisements, monetizing, or venture-capital in Lemmy; software should be communal, and should benefit humanity, not a small group of company owners.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
- [with manual Docker installation](https://lemmy.ml/docs/administration_install_docker.html#updating)
|
||||||
|
- [with Ansible installation](https://lemmy.ml/docs/administration_install_ansible.html)
|
||||||
|
|
||||||
|
## Testing Federation
|
||||||
|
|
||||||
|
Federation is finally ready in Lemmy, pending possible bugs or other issues. So for now we suggest to enable federation only on test servers, or try it on our own test servers ( [enterprise](https://enterprise.lemmy.ml/), [ds9](https://ds9.lemmy.ml/), [voyager](https://voyager.lemmy.ml/) ).
|
||||||
|
|
||||||
|
If everything goes well, after a few weeks we will enable federation on lemmy.ml, at first with a limited number of trusted instances. We will also likely change the domain to https://lemmy.ml . Keep in mind that changing domains after turning on federation will break things.
|
||||||
|
|
||||||
|
To enable on your instance, edit your [lemmy.hjson](https://github.com/LemmyNet/lemmy/blob/main/config/defaults.hjson#L60) federation section to `enabled: true`, and restart.
|
||||||
|
|
||||||
|
### Connecting to another server
|
||||||
|
|
||||||
|
The server https://ds9.lemmy.ml has open federation, so after either adding it to the `allowed_instances` list in your `config.hjson`, or if you have open federation, you don't need to add it explicitly.
|
||||||
|
|
||||||
|
To federate / connect with a server, type in `!community_name@server.tld`, in your server's search box [like so](https://voyager.lemmy.ml/search/q/!main%40ds9.lemmy.ml/type/All/sort/TopAll/page/1).
|
||||||
|
|
||||||
|
To connect with the `main` community on ds9, the search is `!main@ds9.lemmy.ml`.
|
||||||
|
|
||||||
|
You can then click the community, and you will see a local version of the community, which you can subscribe to. New posts and comments from `!main@ds9.lemmy.ml` will now show up on your front page, or `/c/All`
|
||||||
|
|
||||||
|
# Lemmy v0.7.40 Pre-Release (2020-08-05)
|
||||||
|
|
||||||
|
We've [added a lot](https://github.com/LemmyNet/lemmy/compare/v0.7.40...v0.7.0) in this pre-release:
|
||||||
|
|
||||||
|
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
|
||||||
|
- Customizeable site icon and banner, user icon and banner, and community icon and banner.
|
||||||
|
- Added user preferred names / display names, bios, and cakedays.
|
||||||
|
- User settings are now shared across browsers (a page refresh will pick up changes).
|
||||||
|
- Visual / Audio captchas through the lemmy API.
|
||||||
|
- Lots of UI prettiness.
|
||||||
|
- Lots of bug fixes.
|
||||||
|
- Lots of additional translations.
|
||||||
|
- Lots of federation prepping / additions / refactors.
|
||||||
|
|
||||||
|
This release removes the need for you to have a pictrs nginx route (the requests are now routed through lemmy directly). Follow the upgrade instructions below to replace your nginx with the new one.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
**With Ansible:**
|
||||||
|
|
||||||
|
```
|
||||||
|
# run these commands locally
|
||||||
|
git pull
|
||||||
|
cd ansible
|
||||||
|
ansible-playbook lemmy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
sudo docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Lemmy v0.7.0 Release (2020-06-23)
|
||||||
|
|
||||||
|
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://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://lemmy.ml
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Use [Github's security advisory issue system](https://github.com/LemmyNet/lemmy/security/advisories/new).
|
|
|
@ -0,0 +1 @@
|
||||||
|
v0.8.10
|
|
@ -0,0 +1,6 @@
|
||||||
|
[defaults]
|
||||||
|
inventory = inventory
|
||||||
|
interpreter_python = /usr/bin/python3
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
|
@ -0,0 +1,12 @@
|
||||||
|
[lemmy]
|
||||||
|
# to get started, copy this file to `inventory` and adjust the values below.
|
||||||
|
# - `myuser@example.com`: replace with the destination you use to connect to your server via ssh
|
||||||
|
# - `domain=example.com`: replace `example.com` with your lemmy domain
|
||||||
|
# - `letsencrypt_contact_email=your@email.com` replace `your@email.com` with your email address,
|
||||||
|
# to get notifications if your ssl cert expires
|
||||||
|
# - `lemmy_base_dir=/srv/lemmy`: the location on the server where lemmy can be installed, can be any folder
|
||||||
|
# if you are upgrading from a previous version, set this to `/lemmy`
|
||||||
|
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com lemmy_base_dir=/srv/lemmy
|
||||||
|
|
||||||
|
[all:vars]
|
||||||
|
ansible_connection=ssh
|
|
@ -0,0 +1,119 @@
|
||||||
|
---
|
||||||
|
- hosts: all
|
||||||
|
|
||||||
|
# Install python if required
|
||||||
|
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
|
||||||
|
gather_facts: False
|
||||||
|
pre_tasks:
|
||||||
|
- name: check lemmy_base_dir
|
||||||
|
fail:
|
||||||
|
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
|
||||||
|
when: lemmy_base_dir is not defined
|
||||||
|
|
||||||
|
- name: install python for Ansible
|
||||||
|
# python2-minimal instead of python-minimal for ubuntu 20.04 and up
|
||||||
|
raw: test -e /usr/bin/python || (apt -y update && apt install -y python3-minimal python3-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'
|
||||||
|
|
||||||
|
- name: install certbot-nginx on ubuntu < 20
|
||||||
|
apt:
|
||||||
|
pkg:
|
||||||
|
- 'python-certbot-nginx'
|
||||||
|
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '<')
|
||||||
|
|
||||||
|
- name: install certbot-nginx on ubuntu > 20
|
||||||
|
apt:
|
||||||
|
pkg:
|
||||||
|
- 'python3-certbot-nginx'
|
||||||
|
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '>=')
|
||||||
|
|
||||||
|
- 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_base_dir}}'
|
||||||
|
owner: 'root'
|
||||||
|
- path: '{{lemmy_base_dir}}/volumes/'
|
||||||
|
owner: 'root'
|
||||||
|
- path: '{{lemmy_base_dir}}/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_base_dir}}/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_base_dir}}/iframely.config.local.js'
|
||||||
|
mode: '0600'
|
||||||
|
vars:
|
||||||
|
lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}"
|
||||||
|
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
|
||||||
|
lemmy_port: "8536"
|
||||||
|
lemmy_ui_port: "1235"
|
||||||
|
pictshare_port: "8537"
|
||||||
|
iframely_port: "8538"
|
||||||
|
|
||||||
|
- name: add config file (only during initial setup)
|
||||||
|
template:
|
||||||
|
src: 'templates/config.hjson'
|
||||||
|
dest: '{{lemmy_base_dir}}/lemmy.hjson'
|
||||||
|
mode: '0600'
|
||||||
|
force: false
|
||||||
|
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_base_dir}}'
|
||||||
|
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'"
|
|
@ -0,0 +1,141 @@
|
||||||
|
---
|
||||||
|
- 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: check lemmy_base_dir
|
||||||
|
fail:
|
||||||
|
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
|
||||||
|
when: lemmy_base_dir is not defined
|
||||||
|
|
||||||
|
- 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_base_dir}}/lemmy/'
|
||||||
|
owner: 'root'
|
||||||
|
- path: '{{lemmy_base_dir}}/volumes/'
|
||||||
|
owner: 'root'
|
||||||
|
- path: '{{lemmy_base_dir}}/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_base_dir}}/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_base_dir}}/iframely.config.local.js'
|
||||||
|
mode: '0600'
|
||||||
|
|
||||||
|
- name: add config file (only during initial setup)
|
||||||
|
template:
|
||||||
|
src: 'templates/config.hjson'
|
||||||
|
dest: '{{lemmy_base_dir}}/lemmy.hjson'
|
||||||
|
mode: '0600'
|
||||||
|
force: false
|
||||||
|
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_base_dir}}/lemmy-dev.tar'
|
||||||
|
|
||||||
|
- name: import docker image
|
||||||
|
docker_image:
|
||||||
|
name: lemmy
|
||||||
|
tag: dev
|
||||||
|
load_path: '{{lemmy_base_dir}}/lemmy-dev.tar'
|
||||||
|
source: load
|
||||||
|
force_source: yes
|
||||||
|
register: image_import
|
||||||
|
|
||||||
|
- name: delete remote image file
|
||||||
|
file:
|
||||||
|
path: '{{lemmy_base_dir}}/lemmy-dev.tar'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: delete local image file
|
||||||
|
local_action:
|
||||||
|
module: 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_base_dir}}'
|
||||||
|
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'"
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
# for more info about the config, check out the documentation
|
||||||
|
# https://lemmy.ml/docs/administration_configuration.html
|
||||||
|
|
||||||
|
# settings related to the postgresql database
|
||||||
|
database: {
|
||||||
|
# password to connect to postgres
|
||||||
|
password: "{{ postgres_password }}"
|
||||||
|
# host where postgres is running
|
||||||
|
host: "postgres"
|
||||||
|
}
|
||||||
|
# the domain name of your instance (eg "lemmy.ml")
|
||||||
|
hostname: "{{ domain }}"
|
||||||
|
# json web token for authorization between server and client
|
||||||
|
jwt_secret: "{{ jwt_password }}"
|
||||||
|
# email sending configuration
|
||||||
|
email: {
|
||||||
|
# hostname of the smtp server
|
||||||
|
smtp_server: "postfix:25"
|
||||||
|
# address to send emails from, eg "noreply@your-instance.com"
|
||||||
|
smtp_from_address: "noreply@{{ domain }}"
|
||||||
|
use_tls: false
|
||||||
|
}
|
||||||
|
# settings related to activitypub federation
|
||||||
|
federation: {
|
||||||
|
# whether to enable activitypub federation.
|
||||||
|
enabled: false
|
||||||
|
# Allows and blocks are described here:
|
||||||
|
# https://lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
|
||||||
|
#
|
||||||
|
# comma separated list of instances with which federation is allowed
|
||||||
|
# allowed_instances: ""
|
||||||
|
# comma separated list of instances which are blocked from federating
|
||||||
|
# blocked_instances: ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
lemmy-ui:
|
||||||
|
image: {{ lemmy_docker_ui_image }}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:1235:1234"
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- LEMMY_INTERNAL_HOST=lemmy:8536
|
||||||
|
- LEMMY_EXTERNAL_HOST={{ domain }}
|
||||||
|
- LEMMY_HTTPS=true
|
||||||
|
depends_on:
|
||||||
|
- lemmy
|
||||||
|
|
||||||
|
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:v0.2.5-r0
|
||||||
|
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"
|
|
@ -0,0 +1,113 @@
|
||||||
|
limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
# frontend
|
||||||
|
location / {
|
||||||
|
# The default ports:
|
||||||
|
# lemmy_ui_port: 1235
|
||||||
|
# lemmy_port: 8536
|
||||||
|
|
||||||
|
set $proxpass "http://0.0.0.0:{{ lemmy_ui_port }}";
|
||||||
|
if ($http_accept = "application/activity+json") {
|
||||||
|
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
|
||||||
|
}
|
||||||
|
if ($request_method = POST) {
|
||||||
|
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
|
||||||
|
}
|
||||||
|
proxy_pass $proxpass;
|
||||||
|
|
||||||
|
rewrite ^(.+)/+$ $1 permanent;
|
||||||
|
}
|
||||||
|
|
||||||
|
# backend
|
||||||
|
location ~ ^/(api|docs|pictrs|feeds|nodeinfo|.well-known) {
|
||||||
|
proxy_pass http://0.0.0.0:{{ lemmy_port }};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Rate limit
|
||||||
|
limit_req zone=lemmy_ratelimit burst=30 nodelay;
|
||||||
|
|
||||||
|
# Add IP forwarding headers
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Redirect pictshare images to pictrs
|
||||||
|
location ~ /pictshare/(.*)$ {
|
||||||
|
return 301 /pictrs/image/$1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
- 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_base_dir}}'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: delete data
|
||||||
|
file:
|
||||||
|
path: '{{item.path}}'
|
||||||
|
state: absent
|
||||||
|
with_items:
|
||||||
|
- path: '{{lemmy_base_dir}}'
|
||||||
|
- 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,42 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json",
|
|
||||||
"warnOnUnsupportedTypeScriptVersion": false
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
||||||
"@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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
package-manager-strict=false
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"arrowParens": "avoid",
|
|
||||||
"semi": true
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: "ts-jest",
|
preset: 'ts-jest',
|
||||||
testEnvironment: "node",
|
testEnvironment: 'node',
|
||||||
};
|
};
|
|
@ -6,31 +6,15 @@
|
||||||
"repository": "https://github.com/LemmyNet/lemmy",
|
"repository": "https://github.com/LemmyNet/lemmy",
|
||||||
"author": "Dessalines",
|
"author": "Dessalines",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
|
"api-test": "jest src/ -i --verbose"
|
||||||
"fix": "prettier --write src && eslint --fix src",
|
|
||||||
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
|
|
||||||
"api-test-follow": "jest -i follow.spec.ts",
|
|
||||||
"api-test-comment": "jest -i comment.spec.ts",
|
|
||||||
"api-test-post": "jest -i post.spec.ts",
|
|
||||||
"api-test-user": "jest -i user.spec.ts",
|
|
||||||
"api-test-community": "jest -i community.spec.ts",
|
|
||||||
"api-test-private-message": "jest -i private_message.spec.ts",
|
|
||||||
"api-test-image": "jest -i image.spec.ts"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^26.0.14",
|
||||||
"@types/node": "^20.12.4",
|
"jest": "^26.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"lemmy-js-client": "^1.0.14",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"node-fetch": "^2.6.1",
|
||||||
"download-file-sync": "^1.0.4",
|
"ts-jest": "^26.4.1",
|
||||||
"eslint": "^8.57.0",
|
"typescript": "^4.0.3"
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"jest": "^29.5.0",
|
|
||||||
"lemmy-js-client": "0.19.4-alpha.18",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"ts-jest": "^29.1.0",
|
|
||||||
"typescript": "^5.4.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,94 +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
|
|
||||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/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/v3/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "alpha started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-beta:8551/api/v3/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "beta started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-gamma:8561/api/v3/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "gamma started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-delta:8571/api/v3/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "delta started"
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-epsilon:8581/api/v3/site')" != "200" ]]; do sleep 1; done
|
|
||||||
echo "epsilon started. All started"
|
|
|
@ -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
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,535 +1,157 @@
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
beta,
|
beta,
|
||||||
gamma,
|
|
||||||
setupLogins,
|
setupLogins,
|
||||||
resolveCommunity,
|
searchForBetaCommunity,
|
||||||
|
searchForCommunity,
|
||||||
createCommunity,
|
createCommunity,
|
||||||
deleteCommunity,
|
deleteCommunity,
|
||||||
removeCommunity,
|
removeCommunity,
|
||||||
getCommunity,
|
getCommunity,
|
||||||
followCommunity,
|
followCommunity,
|
||||||
banPersonFromCommunity,
|
delay,
|
||||||
resolvePerson,
|
|
||||||
getSite,
|
|
||||||
createPost,
|
|
||||||
getPost,
|
|
||||||
resolvePost,
|
|
||||||
registerUser,
|
|
||||||
getPosts,
|
|
||||||
getComments,
|
|
||||||
createComment,
|
|
||||||
getCommunityByName,
|
|
||||||
blockInstance,
|
|
||||||
waitUntil,
|
|
||||||
alphaUrl,
|
|
||||||
delta,
|
|
||||||
betaAllowedInstances,
|
|
||||||
searchPostLocal,
|
|
||||||
longDelay,
|
longDelay,
|
||||||
editCommunity,
|
} from './shared';
|
||||||
unfollows,
|
import {
|
||||||
} from "./shared";
|
Community,
|
||||||
import { EditCommunity, EditSite } from "lemmy-js-client";
|
} from 'lemmy-js-client';
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
beforeAll(async () => {
|
||||||
afterAll(unfollows);
|
await setupLogins();
|
||||||
|
});
|
||||||
|
|
||||||
function assertCommunityFederation(
|
function assertCommunityFederation(
|
||||||
communityOne?: CommunityView,
|
communityOne: Community,
|
||||||
communityTwo?: CommunityView,
|
communityTwo: Community) {
|
||||||
) {
|
expect(communityOne.actor_id).toBe(communityTwo.actor_id);
|
||||||
expect(communityOne?.community.actor_id).toBe(
|
expect(communityOne.name).toBe(communityTwo.name);
|
||||||
communityTwo?.community.actor_id,
|
expect(communityOne.title).toBe(communityTwo.title);
|
||||||
);
|
expect(communityOne.description).toBe(communityTwo.description);
|
||||||
expect(communityOne?.community.name).toBe(communityTwo?.community.name);
|
expect(communityOne.icon).toBe(communityTwo.icon);
|
||||||
expect(communityOne?.community.title).toBe(communityTwo?.community.title);
|
expect(communityOne.banner).toBe(communityTwo.banner);
|
||||||
expect(communityOne?.community.description).toBe(
|
expect(communityOne.published).toBe(communityTwo.published);
|
||||||
communityTwo?.community.description,
|
expect(communityOne.creator_actor_id).toBe(communityTwo.creator_actor_id);
|
||||||
);
|
expect(communityOne.nsfw).toBe(communityTwo.nsfw);
|
||||||
expect(communityOne?.community.icon).toBe(communityTwo?.community.icon);
|
expect(communityOne.category_id).toBe(communityTwo.category_id);
|
||||||
expect(communityOne?.community.banner).toBe(communityTwo?.community.banner);
|
expect(communityOne.removed).toBe(communityTwo.removed);
|
||||||
expect(communityOne?.community.published).toBe(
|
expect(communityOne.deleted).toBe(communityTwo.deleted);
|
||||||
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 () => {
|
test('Create community', async () => {
|
||||||
let communityRes = await createCommunity(alpha);
|
let communityRes = await createCommunity(alpha);
|
||||||
expect(communityRes.community_view.community.name).toBeDefined();
|
expect(communityRes.community.name).toBeDefined();
|
||||||
|
|
||||||
// A dupe check
|
// A dupe check
|
||||||
let prevName = communityRes.community_view.community.name;
|
let prevName = communityRes.community.name;
|
||||||
await expect(createCommunity(alpha, prevName)).rejects.toStrictEqual(
|
let communityRes2 = await createCommunity(alpha, prevName);
|
||||||
Error("community_already_exists"),
|
expect(communityRes2['error']).toBe('community_already_exists');
|
||||||
);
|
await delay();
|
||||||
|
|
||||||
// Cache the community on beta, make sure it has the other fields
|
// Cache the community on beta, make sure it has the other fields
|
||||||
let searchShort = `!${prevName}@lemmy-alpha:8541`;
|
let searchShort = `!${prevName}@lemmy-alpha:8541`;
|
||||||
let betaCommunity = (await resolveCommunity(beta, searchShort)).community;
|
let search = await searchForCommunity(beta, searchShort);
|
||||||
assertCommunityFederation(betaCommunity, communityRes.community_view);
|
let communityOnBeta = search.communities[0];
|
||||||
|
assertCommunityFederation(communityOnBeta, communityRes.community);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Delete community", async () => {
|
test('Delete community', async () => {
|
||||||
let communityRes = await createCommunity(beta);
|
let communityRes = await createCommunity(beta);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Cache the community on Alpha
|
// Cache the community on Alpha
|
||||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
let searchShort = `!${communityRes.community.name}@lemmy-beta:8551`;
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
let search = await searchForCommunity(alpha, searchShort);
|
||||||
if (!alphaCommunity) {
|
let communityOnAlpha = search.communities[0];
|
||||||
throw "Missing alpha community";
|
assertCommunityFederation(communityOnAlpha, communityRes.community);
|
||||||
}
|
await delay();
|
||||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
|
||||||
|
|
||||||
// Follow the community from alpha
|
// Follow the community from alpha
|
||||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
let follow = await followCommunity(alpha, true, communityOnAlpha.id);
|
||||||
|
|
||||||
// Make sure the follow response went through
|
// Make sure the follow response went through
|
||||||
expect(follow.community_view.community.local).toBe(false);
|
expect(follow.community.local).toBe(false);
|
||||||
|
await delay();
|
||||||
|
|
||||||
let deleteCommunityRes = await deleteCommunity(
|
let deleteCommunityRes = await deleteCommunity(
|
||||||
beta,
|
beta,
|
||||||
true,
|
true,
|
||||||
communityRes.community_view.community.id,
|
communityRes.community.id
|
||||||
);
|
|
||||||
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
|
|
||||||
expect(deleteCommunityRes.community_view.community.title).toBe(
|
|
||||||
communityRes.community_view.community.title,
|
|
||||||
);
|
);
|
||||||
|
expect(deleteCommunityRes.community.deleted).toBe(true);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Make sure it got deleted on A
|
// Make sure it got deleted on A
|
||||||
let communityOnAlphaDeleted = await waitUntil(
|
let communityOnAlphaDeleted = await getCommunity(alpha, communityOnAlpha.id);
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
expect(communityOnAlphaDeleted.community.deleted).toBe(true);
|
||||||
g => g.community_view.community.deleted,
|
await delay();
|
||||||
);
|
|
||||||
expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true);
|
|
||||||
|
|
||||||
// Undelete
|
// Undelete
|
||||||
let undeleteCommunityRes = await deleteCommunity(
|
let undeleteCommunityRes = await deleteCommunity(
|
||||||
beta,
|
beta,
|
||||||
false,
|
false,
|
||||||
communityRes.community_view.community.id,
|
communityRes.community.id
|
||||||
);
|
);
|
||||||
expect(undeleteCommunityRes.community_view.community.deleted).toBe(false);
|
expect(undeleteCommunityRes.community.deleted).toBe(false);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Make sure it got undeleted on A
|
// Make sure it got undeleted on A
|
||||||
let communityOnAlphaUnDeleted = await waitUntil(
|
let communityOnAlphaUnDeleted = await getCommunity(alpha, communityOnAlpha.id);
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
expect(communityOnAlphaUnDeleted.community.deleted).toBe(false);
|
||||||
g => !g.community_view.community.deleted,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Remove community", async () => {
|
test('Remove community', async () => {
|
||||||
let communityRes = await createCommunity(beta);
|
let communityRes = await createCommunity(beta);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Cache the community on Alpha
|
// Cache the community on Alpha
|
||||||
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
|
let searchShort = `!${communityRes.community.name}@lemmy-beta:8551`;
|
||||||
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community;
|
let search = await searchForCommunity(alpha, searchShort);
|
||||||
if (!alphaCommunity) {
|
let communityOnAlpha = search.communities[0];
|
||||||
throw "Missing alpha community";
|
assertCommunityFederation(communityOnAlpha, communityRes.community);
|
||||||
}
|
await delay();
|
||||||
assertCommunityFederation(alphaCommunity, communityRes.community_view);
|
|
||||||
|
|
||||||
// Follow the community from alpha
|
// Follow the community from alpha
|
||||||
let follow = await followCommunity(alpha, true, alphaCommunity.community.id);
|
let follow = await followCommunity(alpha, true, communityOnAlpha.id);
|
||||||
|
|
||||||
// Make sure the follow response went through
|
// Make sure the follow response went through
|
||||||
expect(follow.community_view.community.local).toBe(false);
|
expect(follow.community.local).toBe(false);
|
||||||
|
await delay();
|
||||||
|
|
||||||
let removeCommunityRes = await removeCommunity(
|
let removeCommunityRes = await removeCommunity(
|
||||||
beta,
|
beta,
|
||||||
true,
|
true,
|
||||||
communityRes.community_view.community.id,
|
communityRes.community.id
|
||||||
);
|
|
||||||
expect(removeCommunityRes.community_view.community.removed).toBe(true);
|
|
||||||
expect(removeCommunityRes.community_view.community.title).toBe(
|
|
||||||
communityRes.community_view.community.title,
|
|
||||||
);
|
);
|
||||||
|
expect(removeCommunityRes.community.removed).toBe(true);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Make sure it got Removed on A
|
// Make sure it got Removed on A
|
||||||
let communityOnAlphaRemoved = await waitUntil(
|
let communityOnAlphaRemoved = await getCommunity(alpha, communityOnAlpha.id);
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
expect(communityOnAlphaRemoved.community.removed).toBe(true);
|
||||||
g => g.community_view.community.removed,
|
await delay();
|
||||||
);
|
|
||||||
expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true);
|
|
||||||
|
|
||||||
// unremove
|
// unremove
|
||||||
let unremoveCommunityRes = await removeCommunity(
|
let unremoveCommunityRes = await removeCommunity(
|
||||||
beta,
|
beta,
|
||||||
false,
|
false,
|
||||||
communityRes.community_view.community.id,
|
communityRes.community.id
|
||||||
);
|
);
|
||||||
expect(unremoveCommunityRes.community_view.community.removed).toBe(false);
|
expect(unremoveCommunityRes.community.removed).toBe(false);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Make sure it got undeleted on A
|
// Make sure it got undeleted on A
|
||||||
let communityOnAlphaUnRemoved = await waitUntil(
|
let communityOnAlphaUnRemoved = await getCommunity(alpha, communityOnAlpha.id);
|
||||||
() => getCommunity(alpha, alphaCommunity!.community.id),
|
expect(communityOnAlphaUnRemoved.community.removed).toBe(false);
|
||||||
g => !g.community_view.community.removed,
|
|
||||||
);
|
|
||||||
expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Search for beta community", async () => {
|
test('Search for beta community', async () => {
|
||||||
let communityRes = await createCommunity(beta);
|
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();
|
expect(communityRes.community.name).toBeDefined();
|
||||||
|
await delay();
|
||||||
|
|
||||||
// gamma follows community and posts in it
|
let searchShort = `!${communityRes.community.name}@lemmy-beta:8551`;
|
||||||
let gammaCommunity = (
|
let search = await searchForCommunity(alpha, searchShort);
|
||||||
await resolveCommunity(gamma, communityRes.community.actor_id)
|
let communityOnAlpha = search.communities[0];
|
||||||
).community;
|
assertCommunityFederation(communityOnAlpha, communityRes.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 getSite(gamma)).my_user?.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 blockInstance(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 blockInstance(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
|
|
||||||
let editSiteForm: EditSite = {};
|
|
||||||
editSiteForm.allowed_instances = ["lemmy-epsilon"];
|
|
||||||
await beta.editSite(editSiteForm);
|
|
||||||
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
|
|
||||||
editSiteForm.allowed_instances = betaAllowedInstances;
|
|
||||||
await beta.editSite(editSiteForm);
|
|
||||||
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("couldnt_find_object"));
|
|
||||||
|
|
||||||
// create a post, also cant resolve it
|
|
||||||
let postRes = await createPost(alpha, communityRes.id);
|
|
||||||
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
|
|
||||||
Error("couldnt_find_object"),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,98 +1,46 @@
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
setupLogins,
|
setupLogins,
|
||||||
resolveBetaCommunity,
|
searchForBetaCommunity,
|
||||||
followCommunity,
|
followCommunity,
|
||||||
getSite,
|
checkFollowedCommunities,
|
||||||
waitUntil,
|
unfollowRemotes,
|
||||||
beta,
|
delay,
|
||||||
betaUrl,
|
longDelay,
|
||||||
registerUser,
|
} from './shared';
|
||||||
unfollows,
|
|
||||||
} from "./shared";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
beforeAll(async () => {
|
||||||
|
await setupLogins();
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
test("Follow local community", async () => {
|
|
||||||
let user = await registerUser(beta, betaUrl);
|
|
||||||
|
|
||||||
let community = (await resolveBetaCommunity(user)).community!;
|
|
||||||
expect(community.counts.subscribers).toBe(1);
|
|
||||||
expect(community.counts.subscribers_local).toBe(1);
|
|
||||||
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(2);
|
|
||||||
expect(follow.community_view.counts.subscribers_local).toBe(2);
|
|
||||||
|
|
||||||
// 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(1);
|
|
||||||
expect(unfollow.community_view.counts.subscribers_local).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Follow federated community", async () => {
|
afterAll(async () => {
|
||||||
// It takes about 1 second for the community aggregates to federate
|
await unfollowRemotes(alpha);
|
||||||
let betaCommunity = (
|
});
|
||||||
await waitUntil(
|
|
||||||
() => resolveBetaCommunity(alpha),
|
test('Follow federated community', async () => {
|
||||||
c =>
|
let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
|
||||||
c.community?.counts.subscribers === 1 &&
|
let follow = await followCommunity(alpha, true, search.communities[0].id);
|
||||||
c.community.counts.subscribers_local === 0,
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
if (!betaCommunity) {
|
|
||||||
throw "Missing beta community";
|
|
||||||
}
|
|
||||||
let follow = await followCommunity(alpha, true, betaCommunity.community.id);
|
|
||||||
expect(follow.community_view.subscribed).toBe("Pending");
|
|
||||||
betaCommunity = (
|
|
||||||
await waitUntil(
|
|
||||||
() => resolveBetaCommunity(alpha),
|
|
||||||
c => c.community?.subscribed === "Subscribed",
|
|
||||||
)
|
|
||||||
).community;
|
|
||||||
|
|
||||||
// Make sure the follow response went through
|
// Make sure the follow response went through
|
||||||
expect(betaCommunity?.community.local).toBe(false);
|
expect(follow.community.local).toBe(false);
|
||||||
expect(betaCommunity?.community.name).toBe("main");
|
expect(follow.community.name).toBe('main');
|
||||||
expect(betaCommunity?.subscribed).toBe("Subscribed");
|
await longDelay();
|
||||||
expect(betaCommunity?.counts.subscribers_local).toBe(1);
|
|
||||||
|
|
||||||
// check that unfollow was federated
|
|
||||||
let communityOnBeta1 = await resolveBetaCommunity(beta);
|
|
||||||
expect(communityOnBeta1.community?.counts.subscribers).toBe(2);
|
|
||||||
expect(communityOnBeta1.community?.counts.subscribers_local).toBe(1);
|
|
||||||
|
|
||||||
// Check it from local
|
// Check it from local
|
||||||
let site = await getSite(alpha);
|
let followCheck = await checkFollowedCommunities(alpha);
|
||||||
let remoteCommunityId = site.my_user?.follows.find(
|
await delay();
|
||||||
c => c.community.local == false,
|
let remoteCommunityId = followCheck.communities.filter(
|
||||||
)?.community.id;
|
c => c.community_local == false
|
||||||
|
)[0].community_id;
|
||||||
expect(remoteCommunityId).toBeDefined();
|
expect(remoteCommunityId).toBeDefined();
|
||||||
expect(site.my_user?.follows.length).toBe(2);
|
|
||||||
|
|
||||||
if (!remoteCommunityId) {
|
|
||||||
throw "Missing remote community id";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test an unfollow
|
// Test an unfollow
|
||||||
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
|
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
|
||||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
expect(unfollow.community.local).toBe(false);
|
||||||
|
await delay();
|
||||||
|
|
||||||
// Make sure you are unsubbed locally
|
// Make sure you are unsubbed locally
|
||||||
let siteUnfollowCheck = await getSite(alpha);
|
let unfollowCheck = await checkFollowedCommunities(alpha);
|
||||||
expect(siteUnfollowCheck.my_user?.follows.length).toBe(1);
|
expect(unfollowCheck.communities.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
// check that unfollow was federated
|
|
||||||
let communityOnBeta2 = await resolveBetaCommunity(beta);
|
|
||||||
expect(communityOnBeta2.community?.counts.subscribers).toBe(1);
|
|
||||||
expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,363 +0,0 @@
|
||||||
jest.setTimeout(120000);
|
|
||||||
|
|
||||||
import {
|
|
||||||
UploadImage,
|
|
||||||
DeleteImage,
|
|
||||||
PurgePerson,
|
|
||||||
PurgePost,
|
|
||||||
} from "lemmy-js-client";
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
alphaImage,
|
|
||||||
alphaUrl,
|
|
||||||
beta,
|
|
||||||
betaUrl,
|
|
||||||
createCommunity,
|
|
||||||
createPost,
|
|
||||||
deleteAllImages,
|
|
||||||
epsilon,
|
|
||||||
followCommunity,
|
|
||||||
gamma,
|
|
||||||
getSite,
|
|
||||||
imageFetchLimit,
|
|
||||||
registerUser,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
resolveCommunity,
|
|
||||||
resolvePost,
|
|
||||||
setupLogins,
|
|
||||||
waitForPost,
|
|
||||||
unfollows,
|
|
||||||
getPost,
|
|
||||||
waitUntil,
|
|
||||||
createPostWithThumbnail,
|
|
||||||
sampleImage,
|
|
||||||
sampleSite,
|
|
||||||
} from "./shared";
|
|
||||||
const downloadFileSync = require("download-file-sync");
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await Promise.all([unfollows(), deleteAllImages(alpha)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Upload image and delete it", async () => {
|
|
||||||
// Before running this test, you need to delete all previous images in the DB
|
|
||||||
await deleteAllImages(alpha);
|
|
||||||
|
|
||||||
// Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
|
|
||||||
// in testing mode.
|
|
||||||
const upload_form: UploadImage = {
|
|
||||||
image: Buffer.from("test"),
|
|
||||||
};
|
|
||||||
const upload = await alphaImage.uploadImage(upload_form);
|
|
||||||
expect(upload.files![0].file).toBeDefined();
|
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
|
||||||
expect(upload.url).toBeDefined();
|
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const content = downloadFileSync(upload.url);
|
|
||||||
expect(content.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Ensure that it comes back with the list_media endpoint
|
|
||||||
const listMediaRes = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes.images.length).toBe(1);
|
|
||||||
|
|
||||||
// Ensure that it also comes back with the admin all images
|
|
||||||
const listAllMediaRes = await alphaImage.listAllMedia({
|
|
||||||
limit: imageFetchLimit,
|
|
||||||
});
|
|
||||||
|
|
||||||
// This number comes from all the previous thumbnails fetched in other tests.
|
|
||||||
const previousThumbnails = 1;
|
|
||||||
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
|
|
||||||
|
|
||||||
// The deleteUrl is a combination of the endpoint, delete token, and alias
|
|
||||||
let firstImage = listMediaRes.images[0];
|
|
||||||
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
|
|
||||||
expect(deleteUrl).toBe(upload.delete_url);
|
|
||||||
|
|
||||||
// Make sure the uploader is correct
|
|
||||||
expect(firstImage.person.actor_id).toBe(
|
|
||||||
`http://lemmy-alpha:8541/u/lemmy_alpha`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete image
|
|
||||||
const delete_form: DeleteImage = {
|
|
||||||
token: upload.files![0].delete_token,
|
|
||||||
filename: upload.files![0].file,
|
|
||||||
};
|
|
||||||
const delete_ = await alphaImage.deleteImage(delete_form);
|
|
||||||
expect(delete_).toBe(true);
|
|
||||||
|
|
||||||
// ensure that image is deleted
|
|
||||||
const content2 = downloadFileSync(upload.url);
|
|
||||||
expect(content2).toBe("");
|
|
||||||
|
|
||||||
// Ensure that it shows the image is deleted
|
|
||||||
const deletedListMediaRes = await alphaImage.listMedia();
|
|
||||||
expect(deletedListMediaRes.images.length).toBe(0);
|
|
||||||
|
|
||||||
// Ensure that the admin shows its deleted
|
|
||||||
const deletedListAllMediaRes = await alphaImage.listAllMedia({
|
|
||||||
limit: imageFetchLimit,
|
|
||||||
});
|
|
||||||
expect(deletedListAllMediaRes.images.length).toBe(previousThumbnails - 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Purge user, uploaded image removed", async () => {
|
|
||||||
let user = await registerUser(alphaImage, alphaUrl);
|
|
||||||
|
|
||||||
// upload test image
|
|
||||||
const upload_form: UploadImage = {
|
|
||||||
image: Buffer.from("test"),
|
|
||||||
};
|
|
||||||
const upload = await user.uploadImage(upload_form);
|
|
||||||
expect(upload.files![0].file).toBeDefined();
|
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
|
||||||
expect(upload.url).toBeDefined();
|
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const content = downloadFileSync(upload.url);
|
|
||||||
expect(content.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// purge user
|
|
||||||
let site = await getSite(user);
|
|
||||||
const purgeForm: PurgePerson = {
|
|
||||||
person_id: site.my_user!.local_user_view.person.id,
|
|
||||||
};
|
|
||||||
const delete_ = await alphaImage.purgePerson(purgeForm);
|
|
||||||
expect(delete_.success).toBe(true);
|
|
||||||
|
|
||||||
// ensure that image is deleted
|
|
||||||
const content2 = downloadFileSync(upload.url);
|
|
||||||
expect(content2).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Purge post, linked image removed", async () => {
|
|
||||||
let user = await registerUser(beta, betaUrl);
|
|
||||||
|
|
||||||
// upload test image
|
|
||||||
const upload_form: UploadImage = {
|
|
||||||
image: Buffer.from("test"),
|
|
||||||
};
|
|
||||||
const upload = await user.uploadImage(upload_form);
|
|
||||||
expect(upload.files![0].file).toBeDefined();
|
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
|
||||||
expect(upload.url).toBeDefined();
|
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
|
||||||
const content = downloadFileSync(upload.url);
|
|
||||||
expect(content.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
let community = await resolveBetaCommunity(user);
|
|
||||||
let post = await createPost(
|
|
||||||
user,
|
|
||||||
community.community!.community.id,
|
|
||||||
upload.url,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post.url).toBe(upload.url);
|
|
||||||
|
|
||||||
// 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 content2 = downloadFileSync(upload.url);
|
|
||||||
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();
|
|
||||||
|
|
||||||
// remote image gets proxied after upload
|
|
||||||
expect(
|
|
||||||
post.thumbnail_url?.startsWith(
|
|
||||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
// Make sure that it ends with jpg, to be sure its an image
|
|
||||||
expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
|
||||||
|
|
||||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
|
||||||
expect(epsilonPostRes.post).toBeDefined();
|
|
||||||
|
|
||||||
// Fetch the post again, the metadata should be backgrounded now
|
|
||||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
|
||||||
let epsilonPostRes2 = await waitUntil(
|
|
||||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
|
||||||
p => p.post_view.post.thumbnail_url != undefined,
|
|
||||||
);
|
|
||||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
epsilonPost.thumbnail_url?.startsWith(
|
|
||||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(
|
|
||||||
epsilonPost.body?.startsWith(
|
|
||||||
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
// Make sure that it ends with jpg, to be sure its an image
|
|
||||||
expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Thumbnail of remote image link is proxied if setting enabled", async () => {
|
|
||||||
let community = await createCommunity(gamma);
|
|
||||||
let postRes = await createPost(
|
|
||||||
gamma,
|
|
||||||
community.community_view.community.id,
|
|
||||||
// The sample site metadata thumbnail ends in png
|
|
||||||
sampleSite,
|
|
||||||
);
|
|
||||||
const post = postRes.post_view.post;
|
|
||||||
expect(post).toBeDefined();
|
|
||||||
|
|
||||||
// remote image gets proxied after upload
|
|
||||||
expect(
|
|
||||||
post.thumbnail_url?.startsWith(
|
|
||||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
// Make sure that it ends with png, to be sure its an image
|
|
||||||
expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
|
||||||
|
|
||||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
|
||||||
expect(epsilonPostRes.post).toBeDefined();
|
|
||||||
|
|
||||||
let epsilonPostRes2 = await waitUntil(
|
|
||||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
|
||||||
p => p.post_view.post.thumbnail_url != undefined,
|
|
||||||
);
|
|
||||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
|
||||||
|
|
||||||
expect(
|
|
||||||
epsilonPost.thumbnail_url?.startsWith(
|
|
||||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
|
||||||
),
|
|
||||||
).toBeTruthy();
|
|
||||||
|
|
||||||
// Make sure that it ends with png, to be sure its an image
|
|
||||||
expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("No image proxying if setting is disabled", async () => {
|
|
||||||
let user = await registerUser(beta, betaUrl);
|
|
||||||
let community = await createCommunity(alpha);
|
|
||||||
let betaCommunity = await resolveCommunity(
|
|
||||||
beta,
|
|
||||||
community.community_view.community.actor_id,
|
|
||||||
);
|
|
||||||
await followCommunity(beta, true, betaCommunity.community!.community.id);
|
|
||||||
|
|
||||||
const upload_form: UploadImage = {
|
|
||||||
image: Buffer.from("test"),
|
|
||||||
};
|
|
||||||
const upload = await user.uploadImage(upload_form);
|
|
||||||
let post = await createPost(
|
|
||||||
alpha,
|
|
||||||
community.community_view.community.id,
|
|
||||||
upload.url,
|
|
||||||
`![](${sampleImage})`,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post).toBeDefined();
|
|
||||||
|
|
||||||
// remote image doesn't get proxied after upload
|
|
||||||
expect(
|
|
||||||
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
|
|
||||||
|
|
||||||
let betaPost = await waitForPost(
|
|
||||||
beta,
|
|
||||||
post.post_view.post,
|
|
||||||
res => res?.post.alt_text != null,
|
|
||||||
);
|
|
||||||
expect(betaPost.post).toBeDefined();
|
|
||||||
|
|
||||||
// remote image doesn't get proxied after federation
|
|
||||||
expect(
|
|
||||||
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
|
||||||
).toBeTruthy();
|
|
||||||
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
|
|
||||||
// Make sure the alt text got federated
|
|
||||||
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Make regular post, and give it a custom thumbnail", async () => {
|
|
||||||
const uploadForm1: UploadImage = {
|
|
||||||
image: Buffer.from("testRegular1"),
|
|
||||||
};
|
|
||||||
const upload1 = await alphaImage.uploadImage(uploadForm1);
|
|
||||||
|
|
||||||
const community = await createCommunity(alphaImage);
|
|
||||||
|
|
||||||
// Use wikipedia since it has an opengraph image
|
|
||||||
const wikipediaUrl = "https://wikipedia.org/";
|
|
||||||
|
|
||||||
let post = await createPostWithThumbnail(
|
|
||||||
alphaImage,
|
|
||||||
community.community_view.community.id,
|
|
||||||
wikipediaUrl,
|
|
||||||
upload1.url!,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
|
||||||
post = await waitUntil(
|
|
||||||
() => getPost(alphaImage, post.post_view.post.id),
|
|
||||||
p => p.post_view.post.thumbnail_url != undefined,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post.url).toBe(wikipediaUrl);
|
|
||||||
// Make sure it uses custom thumbnail
|
|
||||||
expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
|
|
||||||
const uploadForm1: UploadImage = {
|
|
||||||
image: Buffer.from("test1"),
|
|
||||||
};
|
|
||||||
const upload1 = await alphaImage.uploadImage(uploadForm1);
|
|
||||||
|
|
||||||
const uploadForm2: UploadImage = {
|
|
||||||
image: Buffer.from("test2"),
|
|
||||||
};
|
|
||||||
const upload2 = await alphaImage.uploadImage(uploadForm2);
|
|
||||||
|
|
||||||
const community = await createCommunity(alphaImage);
|
|
||||||
|
|
||||||
let post = await createPostWithThumbnail(
|
|
||||||
alphaImage,
|
|
||||||
community.community_view.community.id,
|
|
||||||
upload1.url!,
|
|
||||||
upload2.url!,
|
|
||||||
);
|
|
||||||
post = await waitUntil(
|
|
||||||
() => getPost(alphaImage, post.post_view.post.id),
|
|
||||||
p => p.post_view.post.thumbnail_url != undefined,
|
|
||||||
);
|
|
||||||
expect(post.post_view.post.url).toBe(upload1.url);
|
|
||||||
// Make sure the custom thumbnail is ignored
|
|
||||||
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,145 +5,78 @@ import {
|
||||||
setupLogins,
|
setupLogins,
|
||||||
followBeta,
|
followBeta,
|
||||||
createPrivateMessage,
|
createPrivateMessage,
|
||||||
editPrivateMessage,
|
updatePrivateMessage,
|
||||||
listPrivateMessages,
|
listPrivateMessages,
|
||||||
deletePrivateMessage,
|
deletePrivateMessage,
|
||||||
waitUntil,
|
unfollowRemotes,
|
||||||
reportPrivateMessage,
|
delay,
|
||||||
unfollows,
|
longDelay,
|
||||||
} from "./shared";
|
} from './shared';
|
||||||
|
|
||||||
let recipient_id: number;
|
let recipient_id: number;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupLogins();
|
await setupLogins();
|
||||||
await followBeta(alpha);
|
let follow = await followBeta(alpha);
|
||||||
recipient_id = 3;
|
await longDelay();
|
||||||
|
recipient_id = follow.community.creator_id;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(unfollows);
|
afterAll(async () => {
|
||||||
|
await unfollowRemotes(alpha);
|
||||||
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 () => {
|
test('Create a private message', async () => {
|
||||||
let updatedContent = "A jest test federated private message edited";
|
|
||||||
|
|
||||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||||
let pmUpdated = await editPrivateMessage(
|
expect(pmRes.message.content).toBeDefined();
|
||||||
alpha,
|
expect(pmRes.message.local).toBe(true);
|
||||||
pmRes.private_message_view.private_message.id,
|
expect(pmRes.message.creator_local).toBe(true);
|
||||||
);
|
expect(pmRes.message.recipient_local).toBe(false);
|
||||||
expect(pmUpdated.private_message_view.private_message.content).toBe(
|
await delay();
|
||||||
updatedContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
let betaPms = await waitUntil(
|
let betaPms = await listPrivateMessages(beta);
|
||||||
() => listPrivateMessages(beta),
|
expect(betaPms.messages[0].content).toBeDefined();
|
||||||
p => p.private_messages[0].private_message.content === updatedContent,
|
expect(betaPms.messages[0].local).toBe(false);
|
||||||
);
|
expect(betaPms.messages[0].creator_local).toBe(false);
|
||||||
expect(betaPms.private_messages[0].private_message.content).toBe(
|
expect(betaPms.messages[0].recipient_local).toBe(true);
|
||||||
updatedContent,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Delete a private message", async () => {
|
test('Update a private message', async () => {
|
||||||
|
let updatedContent = 'A jest test federated private message edited';
|
||||||
|
|
||||||
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||||
let betaPms1 = await waitUntil(
|
let pmUpdated = await updatePrivateMessage(alpha, pmRes.message.id);
|
||||||
() => listPrivateMessages(beta),
|
expect(pmUpdated.message.content).toBe(updatedContent);
|
||||||
m =>
|
await longDelay();
|
||||||
!!m.private_messages.find(
|
|
||||||
e =>
|
let betaPms = await listPrivateMessages(beta);
|
||||||
e.private_message.ap_id ===
|
expect(betaPms.messages[0].content).toBe(updatedContent);
|
||||||
pmRes.private_message_view.private_message.ap_id,
|
});
|
||||||
),
|
|
||||||
);
|
test('Delete a private message', async () => {
|
||||||
let deletedPmRes = await deletePrivateMessage(
|
let pmRes = await createPrivateMessage(alpha, recipient_id);
|
||||||
alpha,
|
await delay();
|
||||||
true,
|
let betaPms1 = await listPrivateMessages(beta);
|
||||||
pmRes.private_message_view.private_message.id,
|
let deletedPmRes = await deletePrivateMessage(alpha, true, pmRes.message.id);
|
||||||
);
|
expect(deletedPmRes.message.deleted).toBe(true);
|
||||||
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
|
await delay();
|
||||||
|
|
||||||
// The GetPrivateMessages filters out deleted,
|
// The GetPrivateMessages filters out deleted,
|
||||||
// even though they are in the actual database.
|
// even though they are in the actual database.
|
||||||
// no reason to show them
|
// no reason to show them
|
||||||
let betaPms2 = await waitUntil(
|
let betaPms2 = await listPrivateMessages(beta);
|
||||||
() => listPrivateMessages(beta),
|
expect(betaPms2.messages.length).toBe(betaPms1.messages.length - 1);
|
||||||
p => p.private_messages.length === betaPms1.private_messages.length - 1,
|
await delay();
|
||||||
);
|
|
||||||
expect(betaPms2.private_messages.length).toBe(
|
|
||||||
betaPms1.private_messages.length - 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Undelete
|
// Undelete
|
||||||
let undeletedPmRes = await deletePrivateMessage(
|
let undeletedPmRes = await deletePrivateMessage(
|
||||||
alpha,
|
alpha,
|
||||||
false,
|
false,
|
||||||
pmRes.private_message_view.private_message.id,
|
pmRes.message.id
|
||||||
);
|
|
||||||
expect(undeletedPmRes.private_message_view.private_message.deleted).toBe(
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
expect(undeletedPmRes.message.deleted).toBe(false);
|
||||||
|
await longDelay();
|
||||||
|
|
||||||
let betaPms3 = await waitUntil(
|
let betaPms3 = await listPrivateMessages(beta);
|
||||||
() => listPrivateMessages(beta),
|
expect(betaPms3.messages.length).toBe(betaPms1.messages.length);
|
||||||
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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,195 +1,78 @@
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
import { PersonView } from "lemmy-js-client/dist/types/PersonView";
|
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
beta,
|
beta,
|
||||||
registerUser,
|
registerUser,
|
||||||
resolvePerson,
|
searchForUser,
|
||||||
getSite,
|
saveUserSettingsBio,
|
||||||
createPost,
|
|
||||||
resolveCommunity,
|
|
||||||
createComment,
|
|
||||||
resolveBetaCommunity,
|
|
||||||
deleteUser,
|
|
||||||
saveUserSettingsFederated,
|
|
||||||
setupLogins,
|
|
||||||
alphaUrl,
|
|
||||||
saveUserSettings,
|
saveUserSettings,
|
||||||
getPost,
|
getSite,
|
||||||
getComments,
|
} from './shared';
|
||||||
fetchFunction,
|
import {
|
||||||
alphaImage,
|
UserView,
|
||||||
unfollows,
|
UserSettingsForm,
|
||||||
} from "./shared";
|
} from 'lemmy-js-client';
|
||||||
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
|
|
||||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
|
||||||
afterAll(unfollows);
|
|
||||||
|
|
||||||
|
let auth: string;
|
||||||
let apShortname: string;
|
let apShortname: string;
|
||||||
|
|
||||||
function assertUserFederation(userOne?: PersonView, userTwo?: PersonView) {
|
function assertUserFederation(
|
||||||
expect(userOne?.person.name).toBe(userTwo?.person.name);
|
userOne: UserView,
|
||||||
expect(userOne?.person.display_name).toBe(userTwo?.person.display_name);
|
userTwo: UserView) {
|
||||||
expect(userOne?.person.bio).toBe(userTwo?.person.bio);
|
expect(userOne.name).toBe(userTwo.name);
|
||||||
expect(userOne?.person.actor_id).toBe(userTwo?.person.actor_id);
|
expect(userOne.preferred_username).toBe(userTwo.preferred_username);
|
||||||
expect(userOne?.person.avatar).toBe(userTwo?.person.avatar);
|
expect(userOne.bio).toBe(userTwo.bio);
|
||||||
expect(userOne?.person.banner).toBe(userTwo?.person.banner);
|
expect(userOne.actor_id).toBe(userTwo.actor_id);
|
||||||
expect(userOne?.person.published).toBe(userTwo?.person.published);
|
expect(userOne.avatar).toBe(userTwo.avatar);
|
||||||
|
expect(userOne.banner).toBe(userTwo.banner);
|
||||||
|
expect(userOne.published).toBe(userTwo.published);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Create user", async () => {
|
test('Create user', async () => {
|
||||||
let user = await registerUser(alpha, alphaUrl);
|
let userRes = await registerUser(alpha);
|
||||||
|
expect(userRes.jwt).toBeDefined();
|
||||||
|
auth = userRes.jwt;
|
||||||
|
|
||||||
let site = await getSite(user);
|
let site = await getSite(alpha, auth);
|
||||||
expect(site.my_user).toBeDefined();
|
expect(site.my_user).toBeDefined();
|
||||||
if (!site.my_user) {
|
apShortname = `@${site.my_user.name}@lemmy-alpha:8541`;
|
||||||
throw "Missing site user";
|
});
|
||||||
|
|
||||||
|
test('Save user settings, check changed bio from beta', async () => {
|
||||||
|
let bio = 'a changed bio';
|
||||||
|
let userRes = await saveUserSettingsBio(alpha, auth);
|
||||||
|
expect(userRes.jwt).toBeDefined();
|
||||||
|
|
||||||
|
let site = await getSite(alpha, auth);
|
||||||
|
expect(site.my_user.bio).toBe(bio);
|
||||||
|
let searchAlpha = await searchForUser(alpha, site.my_user.actor_id);
|
||||||
|
|
||||||
|
// Make sure beta sees this bio is changed
|
||||||
|
let searchBeta = await searchForUser(beta, apShortname);
|
||||||
|
assertUserFederation(searchAlpha.users[0], searchBeta.users[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Set avatar and banner, check that they are federated', async () => {
|
||||||
|
let avatar = 'https://image.flaticon.com/icons/png/512/35/35896.png';
|
||||||
|
let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png';
|
||||||
|
let form: UserSettingsForm = {
|
||||||
|
show_nsfw: false,
|
||||||
|
theme: "",
|
||||||
|
default_sort_type: 0,
|
||||||
|
default_listing_type: 0,
|
||||||
|
lang: "",
|
||||||
|
avatar,
|
||||||
|
banner,
|
||||||
|
preferred_username: "user321",
|
||||||
|
show_avatars: false,
|
||||||
|
send_notifications_to_email: false,
|
||||||
|
auth,
|
||||||
}
|
}
|
||||||
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
let settingsRes = await saveUserSettings(alpha, form);
|
||||||
});
|
|
||||||
|
let searchAlpha = await searchForUser(beta, apShortname);
|
||||||
test("Set some user settings, check that they are federated", async () => {
|
let userOnAlpha = searchAlpha.users[0];
|
||||||
await saveUserSettingsFederated(alpha);
|
let searchBeta = await searchForUser(beta, apShortname);
|
||||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
let userOnBeta = searchBeta.users[0];
|
||||||
let betaPerson = (await resolvePerson(beta, apShortname)).person;
|
assertUserFederation(userOnAlpha, userOnBeta);
|
||||||
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 site = await getSite(beta);
|
|
||||||
expect(site.my_user?.local_user_view.local_user.theme).toBe("test");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Delete user", async () => {
|
|
||||||
let user = await registerUser(alpha, alphaUrl);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Requests with invalid auth should be treated as unauthenticated", async () => {
|
|
||||||
let invalid_auth = new LemmyHttp(alphaUrl, {
|
|
||||||
headers: { Authorization: "Bearer foobar" },
|
|
||||||
fetchFunction,
|
|
||||||
});
|
|
||||||
let site = await getSite(invalid_auth);
|
|
||||||
expect(site.my_user).toBeUndefined();
|
|
||||||
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 () => {
|
|
||||||
let user = await registerUser(alpha, alphaUrl, "تجريب");
|
|
||||||
|
|
||||||
let site = await getSite(user);
|
|
||||||
expect(site.my_user).toBeDefined();
|
|
||||||
if (!site.my_user) {
|
|
||||||
throw "Missing site user";
|
|
||||||
}
|
|
||||||
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
|
|
||||||
|
|
||||||
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
|
|
||||||
expect(alphaPerson).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Create user with accept-language", async () => {
|
|
||||||
let lemmy_http = new LemmyHttp(alphaUrl, {
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax
|
|
||||||
headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" },
|
|
||||||
});
|
|
||||||
let user = await registerUser(lemmy_http, alphaUrl);
|
|
||||||
|
|
||||||
let site = await getSite(user);
|
|
||||||
expect(site.my_user).toBeDefined();
|
|
||||||
expect(site.my_user?.local_user_view.local_user.interface_language).toBe(
|
|
||||||
"fr",
|
|
||||||
);
|
|
||||||
let langs = site.all_languages
|
|
||||||
.filter(a => site.my_user?.discussion_languages.includes(a.id))
|
|
||||||
.map(l => l.code);
|
|
||||||
// should have languages from accept header, as well as "undetermined"
|
|
||||||
// which is automatically enabled by backend
|
|
||||||
expect(langs).toStrictEqual(["und", "de", "en", "fr"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Set a new avatar, old avatar is deleted", async () => {
|
|
||||||
const listMediaRes = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes.images.length).toBe(0);
|
|
||||||
const upload_form1: UploadImage = {
|
|
||||||
image: Buffer.from("test1"),
|
|
||||||
};
|
|
||||||
const upload1 = await alphaImage.uploadImage(upload_form1);
|
|
||||||
expect(upload1.url).toBeDefined();
|
|
||||||
|
|
||||||
let form1 = {
|
|
||||||
avatar: upload1.url,
|
|
||||||
};
|
|
||||||
await saveUserSettings(alpha, form1);
|
|
||||||
const listMediaRes1 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes1.images.length).toBe(1);
|
|
||||||
|
|
||||||
const upload_form2: UploadImage = {
|
|
||||||
image: Buffer.from("test2"),
|
|
||||||
};
|
|
||||||
const upload2 = await alphaImage.uploadImage(upload_form2);
|
|
||||||
expect(upload2.url).toBeDefined();
|
|
||||||
|
|
||||||
let form2 = {
|
|
||||||
avatar: upload1.url,
|
|
||||||
};
|
|
||||||
await saveUserSettings(alpha, form2);
|
|
||||||
// make sure only the new avatar is kept
|
|
||||||
const listMediaRes2 = await alphaImage.listMedia();
|
|
||||||
expect(listMediaRes2.images.length).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cargo update
|
||||||
|
cargo fmt
|
||||||
|
cargo check
|
||||||
|
cargo clippy
|
||||||
|
cargo outdated -R
|
86
cliff.toml
86
cliff.toml
|
@ -1,86 +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.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 -%}
|
|
||||||
|
|
||||||
{% 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 = "" },
|
|
||||||
]
|
|
||||||
# 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"
|
|
|
@ -1,5 +1,3 @@
|
||||||
# See the documentation for available config fields and descriptions:
|
|
||||||
# https://join-lemmy.org/docs/en/administration/configuration.html
|
|
||||||
{
|
{
|
||||||
hostname: lemmy-alpha
|
hostname: "localhost:8536"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,122 +1,92 @@
|
||||||
{
|
{
|
||||||
|
# # optional: parameters for automatic configuration of new instance (only used at first start)
|
||||||
|
# setup: {
|
||||||
|
# # username for the admin user
|
||||||
|
# admin_username: ""
|
||||||
|
# # password for the admin user
|
||||||
|
# admin_password: ""
|
||||||
|
# # optional: email for the admin user (can be omitted and set later through the website)
|
||||||
|
# admin_email: ""
|
||||||
|
# # name of the site (can be changed later)
|
||||||
|
# site_name: ""
|
||||||
|
# }
|
||||||
# settings related to the postgresql database
|
# settings related to the postgresql database
|
||||||
database: {
|
database: {
|
||||||
# Configure the database by specifying a URI
|
# username to connect to postgres
|
||||||
#
|
user: "lemmy"
|
||||||
# This is the preferred method to specify database connection details since
|
# password to connect to postgres
|
||||||
# it is the most flexible.
|
password: "password"
|
||||||
# Connection URI pointing to a postgres instance
|
# host where postgres is running
|
||||||
#
|
host: "localhost"
|
||||||
# This example uses peer authentication to obviate the need for creating,
|
# port where postgres can be accessed
|
||||||
# configuring, and managing passwords.
|
port: 5432
|
||||||
#
|
# name of the postgres database for lemmy
|
||||||
# For an explanation of how to use connection URIs, see [here][0] in
|
database: "lemmy"
|
||||||
# PostgreSQL's documentation.
|
# maximum number of active sql connections
|
||||||
#
|
pool_size: 5
|
||||||
# [0]: https://www.postgresql.org/docs/current/libpq-connect.html#id-1.7.3.8.3.6
|
|
||||||
uri: "postgresql:///lemmy?user=lemmy&host=/var/run/postgresql"
|
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
# Configure the database by specifying parts of a URI
|
|
||||||
#
|
|
||||||
# Note that specifying the `uri` field should be preferred since it provides
|
|
||||||
# greater control over how the connection is made. This merely exists for
|
|
||||||
# backwards-compatibility.
|
|
||||||
# Username to connect to postgres
|
|
||||||
user: "string"
|
|
||||||
# Password to connect to postgres
|
|
||||||
password: "string"
|
|
||||||
# Host where postgres is running
|
|
||||||
host: "string"
|
|
||||||
# Port where postgres can be accessed
|
|
||||||
port: 123
|
|
||||||
# Name of the postgres database for lemmy
|
|
||||||
database: "string"
|
|
||||||
# Maximum number of active sql connections
|
|
||||||
pool_size: 30
|
|
||||||
}
|
}
|
||||||
# Pictrs image server configuration.
|
# the domain name of your instance (eg "lemmy.ml")
|
||||||
pictrs: {
|
hostname: null
|
||||||
# Address where pictrs is available (for image hosting)
|
# address where lemmy should listen for incoming requests
|
||||||
url: "http://localhost:8080/"
|
|
||||||
# Set a custom pictrs API key. ( Required for deleting images )
|
|
||||||
api_key: "string"
|
|
||||||
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
|
|
||||||
# equivalent to `image_mode: StoreLinkPreviews`.
|
|
||||||
#
|
|
||||||
# To be removed in 0.20
|
|
||||||
cache_external_link_previews: true
|
|
||||||
# Specifies how to handle remote images, so that users don't have to connect directly to remote
|
|
||||||
# servers.
|
|
||||||
image_mode:
|
|
||||||
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
|
|
||||||
# Opengraph image is directly returned as thumbnail
|
|
||||||
"None"
|
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
# Generate thumbnails for external post urls and store them persistently in pict-rs. This
|
|
||||||
# ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
|
|
||||||
# it also increases storage usage.
|
|
||||||
#
|
|
||||||
# This is the default behaviour, and also matches Lemmy 0.18.
|
|
||||||
"StoreLinkPreviews"
|
|
||||||
|
|
||||||
# or
|
|
||||||
|
|
||||||
# If enabled, all images from remote domains are rewritten to pass through
|
|
||||||
# `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
|
|
||||||
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
|
||||||
# servers, and decreases load on other servers. However it increases bandwidth use for the
|
|
||||||
# local server.
|
|
||||||
#
|
|
||||||
# Requires pict-rs 0.5
|
|
||||||
"ProxyAllImages"
|
|
||||||
# Timeout for uploading images to pictrs (in seconds)
|
|
||||||
upload_timeout: 30
|
|
||||||
}
|
|
||||||
# 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"
|
bind: "0.0.0.0"
|
||||||
# Port where lemmy should listen for incoming requests
|
# port where lemmy should listen for incoming requests
|
||||||
port: 8536
|
port: 8536
|
||||||
# Whether the site is available over TLS. Needs to be true for federation to work.
|
# whether tls is required for activitypub. only disable this for debugging, never for producion.
|
||||||
tls_enabled: true
|
tls_enabled: true
|
||||||
# The number of activitypub federation workers that can be in-flight concurrently
|
# json web token for authorization between server and client
|
||||||
worker_count: 0
|
jwt_secret: "changeme"
|
||||||
# The number of activitypub federation retry workers that can be in-flight concurrently
|
# path to built documentation
|
||||||
retry_count: 0
|
docs_dir: "/app/documentation"
|
||||||
prometheus: {
|
# address where pictrs is available
|
||||||
bind: "127.0.0.1"
|
pictrs_url: "http://pictrs:8080"
|
||||||
port: 10002
|
# address where iframely is available
|
||||||
|
iframely_url: "http://iframely"
|
||||||
|
# rate limits for various user actions, by user ip
|
||||||
|
rate_limit: {
|
||||||
|
# maximum number of messages created in interval
|
||||||
|
message: 180
|
||||||
|
# interval length for message limit
|
||||||
|
message_per_second: 60
|
||||||
|
# maximum number of posts created in interval
|
||||||
|
post: 6
|
||||||
|
# interval length for post limit
|
||||||
|
post_per_second: 600
|
||||||
|
# maximum number of registrations in interval
|
||||||
|
register: 3
|
||||||
|
# interval length for registration limit
|
||||||
|
register_per_second: 3600
|
||||||
|
# maximum number of image uploads in interval
|
||||||
|
image: 6
|
||||||
|
# interval length for image uploads
|
||||||
|
image_per_second: 3600
|
||||||
}
|
}
|
||||||
# Sets a response Access-Control-Allow-Origin CORS header
|
# settings related to activitypub federation
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
federation: {
|
||||||
cors_origin: "*"
|
# whether to enable activitypub federation.
|
||||||
|
enabled: false
|
||||||
|
# Allows and blocks are described here:
|
||||||
|
# https://lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
|
||||||
|
#
|
||||||
|
# comma separated list of instances with which federation is allowed
|
||||||
|
allowed_instances: ""
|
||||||
|
# comma separated list of instances which are blocked from federating
|
||||||
|
blocked_instances: ""
|
||||||
|
}
|
||||||
|
captcha: {
|
||||||
|
enabled: true
|
||||||
|
difficulty: medium # Can be easy, medium, or hard
|
||||||
|
}
|
||||||
|
# # email sending configuration
|
||||||
|
# email: {
|
||||||
|
# # hostname and port of the smtp server
|
||||||
|
# smtp_server: ""
|
||||||
|
# # login name for smtp server
|
||||||
|
# smtp_login: ""
|
||||||
|
# # password to login to the smtp server
|
||||||
|
# smtp_password: ""
|
||||||
|
# # address to send emails from, eg "noreply@your-instance.com"
|
||||||
|
# smtp_from_address: ""
|
||||||
|
# # whether or not smtp connections should use tls
|
||||||
|
# use_tls: true
|
||||||
|
# }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +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 }
|
|
||||||
wav = "1.0.1"
|
|
||||||
sitemap-rs = "0.2.1"
|
|
||||||
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
|
|
||||||
actix-web-httpauth = "0.8.1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serial_test = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
elementtree = "1.2.3"
|
|
||||||
pretty_assertions = { workspace = true }
|
|
|
@ -1,66 +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, None)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
orig_comment.community.id,
|
|
||||||
&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.id,
|
|
||||||
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.person.id),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
Ok(Json(CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids: Vec::new(),
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,100 +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_downvotes_enabled},
|
|
||||||
};
|
|
||||||
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 mut recipient_ids = Vec::<LocalUserId>::new();
|
|
||||||
|
|
||||||
// Don't do a downvote if site has downvotes disabled
|
|
||||||
check_downvotes_enabled(data.score, &local_site)?;
|
|
||||||
check_bot_account(&local_user_view.person)?;
|
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
orig_comment.community.id,
|
|
||||||
&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(Some(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,
|
|
||||||
post_id: orig_comment.post.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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(
|
|
||||||
build_comment_response(
|
|
||||||
context.deref(),
|
|
||||||
comment_id,
|
|
||||||
Some(local_user_view),
|
|
||||||
recipient_ids,
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
))
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{ListCommentLikes, ListCommentLikesResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::is_mod_or_admin,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
|
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
|
||||||
|
|
||||||
/// Lists likes for a comment
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_comment_likes(
|
|
||||||
data: Query<ListCommentLikes>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListCommentLikesResponse>> {
|
|
||||||
let comment_view = CommentView::read(
|
|
||||||
&mut context.pool(),
|
|
||||||
data.comment_id,
|
|
||||||
Some(local_user_view.person.id),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
is_mod_or_admin(
|
|
||||||
&mut context.pool(),
|
|
||||||
&local_user_view.person,
|
|
||||||
comment_view.community.id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let comment_likes =
|
|
||||||
VoteView::list_for_comment(&mut context.pool(), data.comment_id, data.page, data.limit).await?;
|
|
||||||
|
|
||||||
Ok(Json(ListCommentLikesResponse { comment_likes }))
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod distinguish;
|
|
||||||
pub mod like;
|
|
||||||
pub mod list_comment_likes;
|
|
||||||
pub mod save;
|
|
|
@ -1,44 +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 {
|
|
||||||
comment_id: data.comment_id,
|
|
||||||
person_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 person_id = local_user_view.person.id;
|
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
Ok(Json(CommentResponse {
|
|
||||||
comment_view,
|
|
||||||
recipient_ids: Vec::new(),
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
use crate::check_report_reason;
|
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{CommentReportResponse, CreateCommentReport},
|
|
||||||
context::LemmyContext,
|
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
|
||||||
utils::{
|
|
||||||
check_comment_deleted_or_removed,
|
|
||||||
check_community_user_action,
|
|
||||||
send_new_report_email_to_admins,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
comment_report::{CommentReport, CommentReportForm},
|
|
||||||
local_site::LocalSite,
|
|
||||||
},
|
|
||||||
traits::Reportable,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{CommentReportView, CommentView, LocalUserView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
/// Creates a comment report and notifies the moderators of the community
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn create_comment_report(
|
|
||||||
data: Json<CreateCommentReport>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
|
||||||
|
|
||||||
let reason = data.reason.trim().to_string();
|
|
||||||
check_report_reason(&reason, &local_site)?;
|
|
||||||
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let comment_id = data.comment_id;
|
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
comment_view.community.id,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Don't allow creating reports for removed / deleted comments
|
|
||||||
check_comment_deleted_or_removed(&comment_view.comment)?;
|
|
||||||
|
|
||||||
let report_form = CommentReportForm {
|
|
||||||
creator_id: person_id,
|
|
||||||
comment_id,
|
|
||||||
original_comment_text: comment_view.comment.content,
|
|
||||||
reason,
|
|
||||||
};
|
|
||||||
|
|
||||||
let report = CommentReport::report(&mut context.pool(), &report_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
|
|
||||||
|
|
||||||
let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
|
||||||
|
|
||||||
// Email the admins
|
|
||||||
if local_site.reports_email_admins {
|
|
||||||
send_new_report_email_to_admins(
|
|
||||||
&comment_report_view.creator.name,
|
|
||||||
&comment_report_view.comment_creator.name,
|
|
||||||
&mut context.pool(),
|
|
||||||
context.settings(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::CreateReport {
|
|
||||||
object_id: comment_view.comment.ap_id.inner().clone(),
|
|
||||||
actor: local_user_view.person,
|
|
||||||
community: comment_view.community,
|
|
||||||
reason: data.reason.clone(),
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(CommentReportResponse {
|
|
||||||
comment_report_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{ListCommentReports, ListCommentReportsResponse},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::check_community_mod_of_any_or_admin_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
/// Lists comment reports for a community if an id is supplied
|
|
||||||
/// or returns all comment reports for communities a user moderates
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn list_comment_reports(
|
|
||||||
data: Query<ListCommentReports>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<ListCommentReportsResponse>> {
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let comment_id = data.comment_id;
|
|
||||||
let unresolved_only = data.unresolved_only.unwrap_or_default();
|
|
||||||
|
|
||||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
|
||||||
|
|
||||||
let page = data.page;
|
|
||||||
let limit = data.limit;
|
|
||||||
let comment_reports = CommentReportQuery {
|
|
||||||
community_id,
|
|
||||||
comment_id,
|
|
||||||
unresolved_only,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
.list(&mut context.pool(), &local_user_view)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(ListCommentReportsResponse { comment_reports }))
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod create;
|
|
||||||
pub mod list;
|
|
||||||
pub mod resolve;
|
|
|
@ -1,51 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
comment::{CommentReportResponse, ResolveCommentReport},
|
|
||||||
context::LemmyContext,
|
|
||||||
utils::check_community_mod_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
|
|
||||||
use lemmy_db_views::structs::{CommentReportView, LocalUserView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
/// Resolves or unresolves a comment report and notifies the moderators of the community
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn resolve_comment_report(
|
|
||||||
data: Json<ResolveCommentReport>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<CommentReportResponse>> {
|
|
||||||
let report_id = data.report_id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let report = CommentReportView::read(&mut context.pool(), report_id, person_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
|
||||||
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
report.community.id,
|
|
||||||
true,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if data.resolved {
|
|
||||||
CommentReport::resolve(&mut context.pool(), report_id, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
|
||||||
} else {
|
|
||||||
CommentReport::unresolve(&mut context.pool(), report_id, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let report_id = data.report_id;
|
|
||||||
let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
|
|
||||||
|
|
||||||
Ok(Json(CommentReportResponse {
|
|
||||||
comment_report_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,97 +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},
|
|
||||||
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_id = data.community_id;
|
|
||||||
|
|
||||||
// Verify that only mods or admins can add mod
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
community_id,
|
|
||||||
false,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let community = Community::read(&mut context.pool(), community_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
// If user is admin and community is remote, explicitly check that he is a
|
|
||||||
// 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 {
|
|
||||||
let is_mod = CommunityModeratorView::is_community_moderator(
|
|
||||||
&mut context.pool(),
|
|
||||||
community.id,
|
|
||||||
local_user_view.person.id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if !is_mod {
|
|
||||||
Err(LemmyErrorType::NotAModerator)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(AddModToCommunityResponse { moderators }))
|
|
||||||
}
|
|
|
@ -1,111 +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_user_data_in_community},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::{
|
|
||||||
CommunityFollower,
|
|
||||||
CommunityFollowerForm,
|
|
||||||
CommunityPersonBan,
|
|
||||||
CommunityPersonBanForm,
|
|
||||||
},
|
|
||||||
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 remove_data = data.remove_data.unwrap_or(false);
|
|
||||||
let expires = check_expire_time(data.expires)?;
|
|
||||||
|
|
||||||
// Verify that only mods or admins can ban
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
data.community_id,
|
|
||||||
false,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
is_valid_body_field(&data.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 {
|
|
||||||
community_id: data.community_id,
|
|
||||||
person_id: banned_person_id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 remove_data {
|
|
||||||
remove_user_data_in_community(data.community_id, banned_person_id, &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)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(BanFromCommunityResponse {
|
|
||||||
person_view,
|
|
||||||
banned: data.ban,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,72 +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 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 {
|
|
||||||
community_id: data.community_id,
|
|
||||||
person_id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
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(person_id), false)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::FollowCommunity(
|
|
||||||
community_view.community.clone(),
|
|
||||||
local_user_view.person.clone(),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(BlockCommunityResponse {
|
|
||||||
blocked: data.block,
|
|
||||||
community_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,77 +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_user_action,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
actor_language::CommunityLanguage,
|
|
||||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
|
||||||
},
|
|
||||||
traits::{Crud, 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 follow_community(
|
|
||||||
data: Json<FollowCommunity>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<CommunityResponse>> {
|
|
||||||
let community = Community::read(&mut context.pool(), data.community_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
let mut community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
person_id: local_user_view.person.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if data.follow {
|
|
||||||
if community.local {
|
|
||||||
check_community_user_action(&local_user_view.person, community.id, &mut context.pool())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
|
||||||
} else {
|
|
||||||
// Mark as pending, the actual federation activity is sent via `SendActivity` handler
|
|
||||||
community_follower_form.pending = true;
|
|
||||||
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityFollowerAlreadyExists)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !community.local {
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::FollowCommunity(community, local_user_view.person.clone(), data.follow),
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let community_view =
|
|
||||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
|
||||||
|
|
||||||
Ok(Json(CommunityResponse {
|
|
||||||
community_view,
|
|
||||||
discussion_languages,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,55 +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},
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
pub mod add_mod;
|
|
||||||
pub mod ban;
|
|
||||||
pub mod block;
|
|
||||||
pub mod follow;
|
|
||||||
pub mod hide;
|
|
||||||
pub mod transfer;
|
|
|
@ -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::{CommunityModerator, CommunityModeratorForm},
|
|
||||||
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_id = data.community_id;
|
|
||||||
let mut community_mods =
|
|
||||||
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
|
||||||
|
|
||||||
check_community_user_action(&local_user_view.person, community_id, &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 person_id = local_user_view.person.id;
|
|
||||||
let community_view =
|
|
||||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(Json(GetCommunityResponse {
|
|
||||||
community_view,
|
|
||||||
site: None,
|
|
||||||
moderators,
|
|
||||||
discussion_languages: vec![],
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,277 +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,
|
|
||||||
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 comment_report;
|
|
||||||
pub mod community;
|
|
||||||
pub mod local_user;
|
|
||||||
pub mod post;
|
|
||||||
pub mod post_report;
|
|
||||||
pub mod private_message;
|
|
||||||
pub mod private_message_report;
|
|
||||||
pub mod site;
|
|
||||||
pub mod sitemap;
|
|
||||||
|
|
||||||
/// Converts the captcha to a base64 encoded wav audio file
|
|
||||||
#[allow(deprecated)]
|
|
||||||
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
|
|
||||||
let letters = captcha.as_wav();
|
|
||||||
|
|
||||||
// Decode each wav file, concatenate the samples
|
|
||||||
let mut concat_samples: Vec<i16> = Vec::new();
|
|
||||||
let mut any_header: Option<wav::Header> = None;
|
|
||||||
for letter in letters {
|
|
||||||
let mut cursor = Cursor::new(letter.unwrap_or_default());
|
|
||||||
let (header, samples) = wav::read(&mut cursor)?;
|
|
||||||
any_header = Some(header);
|
|
||||||
if let Some(samples16) = samples.as_sixteen() {
|
|
||||||
concat_samples.extend(samples16);
|
|
||||||
} else {
|
|
||||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode the concatenated result as a wav file
|
|
||||||
let mut output_buffer = Cursor::new(vec![]);
|
|
||||||
if let Some(header) = any_header {
|
|
||||||
wav::write(
|
|
||||||
header,
|
|
||||||
&wav::BitDepth::Sixteen(concat_samples),
|
|
||||||
&mut output_buffer,
|
|
||||||
)
|
|
||||||
.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()
|
|
||||||
.map_err(|_| 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_data: &Option<bool>,
|
|
||||||
expires: &Option<i64>,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<()> {
|
|
||||||
// Only run this code for federated users
|
|
||||||
if !target.local {
|
|
||||||
let ids = Person::list_local_community_ids(&mut context.pool(), target.id).await?;
|
|
||||||
|
|
||||||
for community_id in ids {
|
|
||||||
let expires_dt = check_expire_time(*expires)?;
|
|
||||||
|
|
||||||
// Ban / unban them from our local communities
|
|
||||||
let community_user_ban_form = CommunityPersonBanForm {
|
|
||||||
community_id,
|
|
||||||
person_id: target.id,
|
|
||||||
expires: Some(expires_dt),
|
|
||||||
};
|
|
||||||
|
|
||||||
if ban {
|
|
||||||
// Ignore all errors for these
|
|
||||||
CommunityPersonBan::ban(&mut context.pool(), &community_user_ban_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Also unsubscribe them from the community, if they are subscribed
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id,
|
|
||||||
person_id: target.id,
|
|
||||||
pending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
CommunityFollower::unfollow(&mut context.pool(), &community_follower_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
CommunityPersonBan::unban(&mut context.pool(), &community_user_ban_form)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mod tables
|
|
||||||
let form = ModBanFromCommunityForm {
|
|
||||||
mod_person_id: local_user_view.person.id,
|
|
||||||
other_person_id: target.id,
|
|
||||||
community_id,
|
|
||||||
reason: reason.clone(),
|
|
||||||
banned: Some(ban),
|
|
||||||
expires: expires_dt,
|
|
||||||
};
|
|
||||||
|
|
||||||
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
|
|
||||||
|
|
||||||
// Federate the ban from community
|
|
||||||
let ban_from_community = BanFromCommunity {
|
|
||||||
community_id,
|
|
||||||
person_id: target.id,
|
|
||||||
ban,
|
|
||||||
reason: reason.clone(),
|
|
||||||
remove_data: *remove_data,
|
|
||||||
expires: *expires,
|
|
||||||
};
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::BanFromCommunity {
|
|
||||||
moderator: local_user_view.person.clone(),
|
|
||||||
community_id,
|
|
||||||
target: target.clone(),
|
|
||||||
data: ban_from_community,
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
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?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
|
|
||||||
check_user_valid(&local_user_view.person)?;
|
|
||||||
|
|
||||||
Ok(local_user_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
#[allow(clippy::indexing_slicing)]
|
|
||||||
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,55 +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},
|
|
||||||
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)?;
|
|
||||||
|
|
||||||
// Make sure that the person_id added is local
|
|
||||||
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(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,105 +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_user_data},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
login_token::LoginToken,
|
|
||||||
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)?;
|
|
||||||
|
|
||||||
is_valid_body_field(&data.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(Some(local_user)) = local_user {
|
|
||||||
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove their data if that's desired
|
|
||||||
let remove_data = data.remove_data.unwrap_or(false);
|
|
||||||
if remove_data {
|
|
||||||
remove_user_data(person.id, &context).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
|
||||||
|
|
||||||
ban_nonlocal_user_from_local_communities(
|
|
||||||
&local_user_view,
|
|
||||||
&person,
|
|
||||||
data.ban,
|
|
||||||
&data.reason,
|
|
||||||
&data.remove_data,
|
|
||||||
&data.expires,
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::BanFromSite {
|
|
||||||
moderator: local_user_view.person,
|
|
||||||
banned_user: person_view.person.clone(),
|
|
||||||
reason: data.reason.clone(),
|
|
||||||
remove_data: data.remove_data,
|
|
||||||
ban: data.ban,
|
|
||||||
expires: data.expires,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(Json(BanPersonResponse {
|
|
||||||
person_view,
|
|
||||||
banned: data.ban,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,59 +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 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()
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
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)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
|
||||||
Ok(Json(BlockPersonResponse {
|
|
||||||
person_view,
|
|
||||||
blocked: data.block,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,53 +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 = verify(
|
|
||||||
&data.old_password,
|
|
||||||
&local_user_view.local_user.password_encrypted,
|
|
||||||
)
|
|
||||||
.unwrap_or(false);
|
|
||||||
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,43 +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::{LemmyErrorExt, 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?
|
|
||||||
.ok_or(LemmyErrorType::TokenNotFound)?
|
|
||||||
.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
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
|
|
||||||
|
|
||||||
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};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
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_view = SiteView::read_local(&mut context.pool())
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
|
||||||
|
|
||||||
if local_user_view.local_user.totp_2fa_enabled {
|
|
||||||
return Err(LemmyErrorType::TotpAlreadyEnabled)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let secret = generate_totp_2fa_secret();
|
|
||||||
let secret_url =
|
|
||||||
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
|
|
||||||
|
|
||||||
let 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,56 +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},
|
|
||||||
};
|
|
||||||
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().expect("failed to generate captcha");
|
|
||||||
|
|
||||||
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;
|
|
||||||
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<Vec<LoginToken>>> {
|
|
||||||
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
|
|
||||||
|
|
||||||
Ok(Json(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,94 +0,0 @@
|
||||||
use crate::{check_totp_2fa_valid, local_user::check_email_verified};
|
|
||||||
use actix_web::{
|
|
||||||
web::{Data, Json},
|
|
||||||
HttpRequest,
|
|
||||||
};
|
|
||||||
use bcrypt::verify;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
claims::Claims,
|
|
||||||
context::LemmyContext,
|
|
||||||
person::{Login, LoginResponse},
|
|
||||||
utils::check_user_valid,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{local_site::LocalSite, registration_application::RegistrationApplication},
|
|
||||||
utils::DbPool,
|
|
||||||
RegistrationMode,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_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?
|
|
||||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
|
||||||
|
|
||||||
// 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?
|
|
||||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
|
||||||
|
|
||||||
// Verify the password
|
|
||||||
let valid: bool = verify(
|
|
||||||
&data.password,
|
|
||||||
&local_user_view.local_user.password_encrypted,
|
|
||||||
)
|
|
||||||
.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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_registration_application(
|
|
||||||
local_user_view: &LocalUserView,
|
|
||||||
local_site: &LocalSite,
|
|
||||||
pool: &mut DbPool<'_>,
|
|
||||||
) -> LemmyResult<()> {
|
|
||||||
if (local_site.registration_mode == RegistrationMode::RequireApplication
|
|
||||||
|| local_site.registration_mode == RegistrationMode::Closed)
|
|
||||||
&& !local_user_view.local_user.accepted_application
|
|
||||||
&& !local_user_view.local_user.admin
|
|
||||||
{
|
|
||||||
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
|
|
||||||
// was processed (either accepted or denied).
|
|
||||||
let local_user_id = local_user_view.local_user.id;
|
|
||||||
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
|
||||||
if registration.admin_id.is_some() {
|
|
||||||
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
|
|
||||||
} else {
|
|
||||||
Err(LemmyErrorType::RegistrationApplicationIsPending)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -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,34 +0,0 @@
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
|
||||||
|
|
||||||
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 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 validate_auth;
|
|
||||||
pub mod verify_email;
|
|
||||||
|
|
||||||
/// Check if the user's email is verified if email verification is turned on
|
|
||||||
/// However, skip checking verification if the user is an admin
|
|
||||||
fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> {
|
|
||||||
if !local_user_view.local_user.admin
|
|
||||||
&& site_view.local_site.require_email_verification
|
|
||||||
&& !local_user_view.local_user.email_verified
|
|
||||||
{
|
|
||||||
Err(LemmyErrorType::EmailNotVerified)?
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -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,49 +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?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
|
|
||||||
|
|
||||||
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_or(LemmyErrorType::CouldntFindPersonMention)?;
|
|
||||||
|
|
||||||
Ok(Json(PersonMentionResponse {
|
|
||||||
person_mention_view,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,48 +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?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
|
|
||||||
|
|
||||||
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_or(LemmyErrorType::CouldntFindCommentReply)?;
|
|
||||||
|
|
||||||
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,26 +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(), person_id).await?;
|
|
||||||
|
|
||||||
let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?;
|
|
||||||
|
|
||||||
let private_messages =
|
|
||||||
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;
|
|
||||||
|
|
||||||
Ok(Json(GetUnreadCountResponse {
|
|
||||||
replies,
|
|
||||||
mentions,
|
|
||||||
private_messages,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,46 +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::{
|
|
||||||
CommentReportView,
|
|
||||||
LocalUserView,
|
|
||||||
PostReportView,
|
|
||||||
PrivateMessageReportView,
|
|
||||||
};
|
|
||||||
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>> {
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
let admin = local_user_view.local_user.admin;
|
|
||||||
let community_id = data.community_id;
|
|
||||||
|
|
||||||
check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?;
|
|
||||||
|
|
||||||
let comment_reports =
|
|
||||||
CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let post_reports =
|
|
||||||
PostReportView::get_report_count(&mut context.pool(), person_id, admin, community_id).await?;
|
|
||||||
|
|
||||||
let private_message_reports = if admin && community_id.is_none() {
|
|
||||||
Some(PrivateMessageReportView::get_report_count(&mut context.pool()).await?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Json(GetReportCountResponse {
|
|
||||||
community_id,
|
|
||||||
comment_reports,
|
|
||||||
post_reports,
|
|
||||||
private_message_reports,
|
|
||||||
}))
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
use crate::local_user::check_email_verified;
|
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::PasswordReset,
|
|
||||||
utils::send_password_reset_email,
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn reset_password(
|
|
||||||
data: Json<PasswordReset>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
// Fetch that email
|
|
||||||
let email = data.email.to_lowercase();
|
|
||||||
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
|
||||||
|
|
||||||
let site_view = SiteView::read_local(&mut context.pool())
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
|
||||||
check_email_verified(&local_user_view, &site_view)?;
|
|
||||||
|
|
||||||
// Email the pure token to the user.
|
|
||||||
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?;
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
use activitypub_federation::config::Data;
|
|
||||||
use actix_web::web::Json;
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
person::SaveUserSettings,
|
|
||||||
request::replace_image,
|
|
||||||
utils::{
|
|
||||||
get_url_blocklist,
|
|
||||||
local_site_to_slur_regex,
|
|
||||||
process_markdown_opt,
|
|
||||||
proxy_image_link_opt_api,
|
|
||||||
send_verification_email,
|
|
||||||
},
|
|
||||||
SuccessResponse,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
actor_language::LocalUserLanguage,
|
|
||||||
local_user::{LocalUser, LocalUserUpdateForm},
|
|
||||||
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
|
|
||||||
person::{Person, PersonUpdateForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
utils::diesel_option_overwrite,
|
|
||||||
};
|
|
||||||
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?
|
|
||||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
|
||||||
|
|
||||||
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
|
|
||||||
let url_blocklist = get_url_blocklist(&context).await?;
|
|
||||||
let bio = diesel_option_overwrite(
|
|
||||||
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?,
|
|
||||||
);
|
|
||||||
replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?;
|
|
||||||
replace_image(&data.banner, &local_user_view.person.banner, &context).await?;
|
|
||||||
|
|
||||||
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
|
|
||||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
|
||||||
let display_name = diesel_option_overwrite(data.display_name.clone());
|
|
||||||
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
|
|
||||||
let email_deref = data.email.as_deref().map(str::to_lowercase);
|
|
||||||
let email = diesel_option_overwrite(email_deref.clone());
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
|
|
||||||
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
|
||||||
}
|
|
||||||
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_sort_type = data.default_sort_type;
|
|
||||||
|
|
||||||
let person_form = PersonUpdateForm {
|
|
||||||
display_name,
|
|
||||||
bio,
|
|
||||||
matrix_user_id,
|
|
||||||
bot_account: data.bot_account,
|
|
||||||
avatar,
|
|
||||||
banner,
|
|
||||||
..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,
|
|
||||||
auto_expand: data.auto_expand,
|
|
||||||
show_bot_accounts: data.show_bot_accounts,
|
|
||||||
show_scores: data.show_scores,
|
|
||||||
default_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,
|
|
||||||
collapse_bot_comments: data.collapse_bot_comments,
|
|
||||||
..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,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,60 +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},
|
|
||||||
},
|
|
||||||
RegistrationMode,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
|
||||||
use lemmy_utils::error::{LemmyErrorType, 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?
|
|
||||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
|
||||||
let token = data.token.clone();
|
|
||||||
let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::TokenNotFound)?;
|
|
||||||
|
|
||||||
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()
|
|
||||||
};
|
|
||||||
let local_user_id = verification.local_user_id;
|
|
||||||
|
|
||||||
LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
|
||||||
|
|
||||||
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
|
||||||
|
|
||||||
// send out notification about registration application to admins if enabled
|
|
||||||
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
|
|
||||||
&& site_view.local_site.application_email_admins
|
|
||||||
{
|
|
||||||
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
|
||||||
|
|
||||||
send_new_applicant_email_to_admins(
|
|
||||||
&local_user.person.name,
|
|
||||||
&mut context.pool(),
|
|
||||||
context.settings(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,82 +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::{
|
|
||||||
moderator::{ModFeaturePost, ModFeaturePostForm},
|
|
||||||
post::{Post, PostUpdateForm},
|
|
||||||
},
|
|
||||||
traits::Crud,
|
|
||||||
PostFeatureType,
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
|
||||||
|
|
||||||
#[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?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
|
||||||
|
|
||||||
check_community_mod_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
orig_post.community_id,
|
|
||||||
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: data.featured,
|
|
||||||
is_featured_community: 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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
build_post_response(
|
|
||||||
&context,
|
|
||||||
orig_post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
|
||||||
use lemmy_api_common::{
|
|
||||||
context::LemmyContext,
|
|
||||||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
|
||||||
request::fetch_link_metadata,
|
|
||||||
};
|
|
||||||
use lemmy_utils::error::LemmyResult;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn get_link_metadata(
|
|
||||||
data: Query<GetSiteMetadata>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
|
||||||
let metadata = fetch_link_metadata(&data.url, &context).await?;
|
|
||||||
|
|
||||||
Ok(Json(GetSiteMetadataResponse { metadata }))
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
use actix_web::web::{Data, Json};
|
|
||||||
use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse};
|
|
||||||
use lemmy_db_schema::source::post::PostHide;
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
|
||||||
pub async fn hide_post(
|
|
||||||
data: Json<HidePost>,
|
|
||||||
context: Data<LemmyContext>,
|
|
||||||
local_user_view: LocalUserView,
|
|
||||||
) -> LemmyResult<Json<SuccessResponse>> {
|
|
||||||
let post_ids = HashSet::from_iter(data.post_ids.clone());
|
|
||||||
|
|
||||||
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
|
|
||||||
Err(LemmyErrorType::TooManyItems)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let person_id = local_user_view.person.id;
|
|
||||||
|
|
||||||
// Mark the post as hidden / unhidden
|
|
||||||
if data.hide {
|
|
||||||
PostHide::hide(&mut context.pool(), post_ids, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
|
||||||
} else {
|
|
||||||
PostHide::unhide(&mut context.pool(), post_ids, person_id)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::default()))
|
|
||||||
}
|
|
|
@ -1,96 +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_downvotes_enabled,
|
|
||||||
mark_post_as_read,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::{
|
|
||||||
source::{
|
|
||||||
community::Community,
|
|
||||||
local_site::LocalSite,
|
|
||||||
post::{Post, PostLike, PostLikeForm},
|
|
||||||
},
|
|
||||||
traits::{Crud, Likeable},
|
|
||||||
};
|
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
|
||||||
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?;
|
|
||||||
|
|
||||||
// Don't do a downvote if site has downvotes disabled
|
|
||||||
check_downvotes_enabled(data.score, &local_site)?;
|
|
||||||
check_bot_account(&local_user_view.person)?;
|
|
||||||
|
|
||||||
// Check for a community ban
|
|
||||||
let post_id = data.post_id;
|
|
||||||
let post = Post::read(&mut context.pool(), post_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
|
||||||
|
|
||||||
check_community_user_action(
|
|
||||||
&local_user_view.person,
|
|
||||||
post.community_id,
|
|
||||||
&mut context.pool(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let like_form = PostLikeForm {
|
|
||||||
post_id: data.post_id,
|
|
||||||
person_id: local_user_view.person.id,
|
|
||||||
score: 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 the post as read
|
|
||||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
|
||||||
|
|
||||||
let community = Community::read(&mut context.pool(), post.community_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
|
||||||
SendActivityData::LikePostOrComment {
|
|
||||||
object_id: post.ap_id,
|
|
||||||
actor: local_user_view.person.clone(),
|
|
||||||
community,
|
|
||||||
score: data.score,
|
|
||||||
},
|
|
||||||
&context,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
build_post_response(
|
|
||||||
context.deref(),
|
|
||||||
post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue