Compare commits

..

No commits in common. "main" and "enforce-post-lock" have entirely different histories.

341 changed files with 18743 additions and 24380 deletions

View file

@ -1,8 +1,8 @@
# build folders and similar which are not needed for the docker build # build folders and similar which are not needed for the docker build
target target
docker docker/dev/volumes
api_tests docker/prod/volumes
ansible docker/federation/volumes
tests docker/travis/volumes
.git .git
*.sh ansible

View file

@ -1,190 +0,0 @@
---
kind: pipeline
name: amd64
platform:
os: linux
arch: amd64
steps:
- name: chown repo
image: ekidd/rust-musl-builder:1.50.0
user: root
commands:
- chown 1000:1000 . -R
- name: check formatting
image: rustdocker/rust:nightly
commands:
- /root/.cargo/bin/cargo fmt -- --check
- name: cargo clippy
image: ekidd/rust-musl-builder:1.50.0
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- whoami
- ls -la ~/.cargo
- mv ~/.cargo .
- ls -la .cargo
- cargo clippy --workspace --tests --all-targets --all-features -- -D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
- cargo clippy --workspace -- -D clippy::unwrap_used
- name: cargo test
image: ekidd/rust-musl-builder:1.50.0
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: 1
RUST_TEST_THREADS: 1
CARGO_HOME: /drone/src/.cargo
commands:
- sudo apt-get update
- sudo apt-get -y install --no-install-recommends espeak postgresql-client
- cargo test --workspace --no-fail-fast
- name: cargo build
image: ekidd/rust-musl-builder:1.50.0
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- cargo build
- mv target/x86_64-unknown-linux-musl/debug/lemmy_server target/lemmy_server
- name: run federation tests
image: node:15-alpine3.12
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: 1
commands:
- apk add bash curl postgresql-client
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
- yarn api-test
- name: make release build and push to docker hub
image: plugins/docker
settings:
dockerfile: docker/prod/Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: dessalines/lemmy
auto_tag: true
auto_tag_suffix: linux-amd64
when:
ref:
- refs/tags/*
- name: push to docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy:${DRONE_TAG}"
template: "dessalines/lemmy:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
services:
- name: database
image: postgres:12-alpine
environment:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password
---
kind: pipeline
name: arm64
platform:
os: linux
arch: arm64
steps:
- name: cargo test
image: rust:1.50-slim-buster
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: 1
RUST_TEST_THREADS: 1
CARGO_HOME: /drone/src/.cargo
commands:
- apt-get update
- apt-get -y install --no-install-recommends espeak postgresql-client libssl-dev pkg-config libpq-dev
- cargo test --workspace --no-fail-fast
- cargo build
# Using Debian here because there seems to be no official Alpine-based Rust docker image for ARM.
- name: cargo build
image: rust:1.50-slim-buster
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- apt-get update
- apt-get -y install --no-install-recommends libssl-dev pkg-config libpq-dev
- cargo build
- mv target/debug/lemmy_server target/lemmy_server
- name: run federation tests
image: node:15-buster-slim
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: 1
commands:
- mkdir -p /usr/share/man/man1 /usr/share/man/man7
- apt-get update
- apt-get -y install --no-install-recommends bash curl libssl-dev pkg-config libpq-dev postgresql-client libc6-dev
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
- yarn api-test
- name: make release build and push to docker hub
image: plugins/docker
settings:
dockerfile: docker/prod/Dockerfile.arm
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: dessalines/lemmy
auto_tag: true
auto_tag_suffix: linux-arm64
when:
ref:
- refs/tags/*
- name: push to docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy:${DRONE_TAG}"
template: "dessalines/lemmy:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
services:
- name: database
image: postgres:12-alpine
environment:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password

3
.gitignore vendored
View file

@ -15,7 +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

View file

@ -1 +0,0 @@
*.sqldump

View file

@ -1,5 +1,5 @@
tab_spaces = 2 tab_spaces = 2
edition="2018" edition="2018"
imports_layout="HorizontalVertical" imports_layout="HorizontalVertical"
imports_granularity="Crate" merge_imports=true
reorder_imports=true reorder_imports=true

30
.travis.yml Normal file
View file

@ -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

35
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,35 @@
# Code of Conduct
- We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
- Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
- Please be kind and courteous. Theres 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 dont 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 youre a regular contributor or a newcomer, we care about making this community a safe place for you and weve 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 communitys 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. Dont just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if theyre 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 couldve communicated better — remember that its 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/).

View file

@ -1,4 +1,4 @@
# Contributing # Contributing
See [here](https://join.lemmy.ml/docs/en/contributing/contributing.html) for contributing Instructions. See [here](https://dev.lemmy.ml/docs/contributing.html) for contributing Instructions.

1850
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,60 +3,51 @@ name = "lemmy_server"
version = "0.0.1" version = "0.0.1"
edition = "2018" edition = "2018"
[lib] [profile.release]
doctest = false lto = true
[profile.dev]
debug = 0
[workspace] [workspace]
members = [ members = [
"crates/api", "lemmy_api",
"crates/apub", "lemmy_apub",
"crates/utils", "lemmy_utils",
"crates/db_queries", "lemmy_db",
"crates/db_schema", "lemmy_structs",
"crates/db_views", "lemmy_rate_limit",
"crates/db_views_actor", "lemmy_websocket",
"crates/db_views_actor",
"crates/api_structs",
"crates/websocket",
"crates/routes"
] ]
[dependencies] [dependencies]
lemmy_api = { path = "./crates/api" } lemmy_api = { path = "./lemmy_api" }
lemmy_apub = { path = "./crates/apub" } lemmy_apub = { path = "./lemmy_apub" }
lemmy_utils = { path = "./crates/utils" } lemmy_utils = { path = "./lemmy_utils" }
lemmy_db_schema = { path = "./crates/db_schema" } lemmy_db = { path = "./lemmy_db" }
lemmy_db_queries = { path = "./crates/db_queries" } lemmy_structs = { path = "./lemmy_structs" }
lemmy_db_views = { path = "./crates/db_views" } lemmy_rate_limit = { path = "./lemmy_rate_limit" }
lemmy_db_views_moderator = { path = "./crates/db_views_moderator" } lemmy_websocket = { path = "./lemmy_websocket" }
lemmy_db_views_actor = { path = "./crates/db_views_actor" } diesel = "1.4"
lemmy_api_structs = { path = "crates/api_structs" } diesel_migrations = "1.4"
lemmy_websocket = { path = "./crates/websocket" } chrono = { version = "0.4", features = ["serde"] }
lemmy_routes = { path = "./crates/routes" } serde = { version = "1.0", features = ["derive"] }
diesel = "1.4.5" actix = "0.10"
diesel_migrations = "1.4.0" actix-web = { version = "3.1", default-features = false, features = ["rustls"] }
chrono = { version = "0.4.19", features = ["serde"] } actix-files = { version = "0.4", default-features = false }
serde = { version = "1.0.123", features = ["derive"] } actix-web-actors = { version = "3.0", default-features = false }
actix = "0.10.0" awc = { version = "2.0", default-features = false }
actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] } log = "0.4"
log = "0.4.14" env_logger = "0.8"
env_logger = "0.8.2" strum = "0.19"
strum = "0.20.0" lazy_static = "1.3"
url = { version = "2.2.1", features = ["serde"] } rss = "1.9"
openssl = "0.10.32" url = { version = "2.1", features = ["serde"] }
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] } openssl = "0.10"
tokio = "0.3.6" http-signature-normalization-actix = { version = "0.4", default-features = false, features = ["sha-2"] }
anyhow = "1.0.38" tokio = "0.3"
reqwest = { version = "0.10.10", features = ["json"] } sha2 = "0.9"
activitystreams = "0.7.0-alpha.10" anyhow = "1.0"
actix-rt = { version = "1.1.1", default-features = false } reqwest = { version = "0.10", features = ["json"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
clokwerk = "0.3.4"
[dev-dependencies.cargo-husky] [dev-dependencies.cargo-husky]
version = "1.5.0" version = "1"
default-features = false # Disable features which are enabled by default default-features = false # Disable features which are enabled by default
features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"] features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"]

View file

@ -1,13 +1,12 @@
<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://cloud.drone.io/api/badges/LemmyNet/lemmy/status.svg)](https://cloud.drone.io/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.yerbamate.ml/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.ml/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)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
</div> </div>
<p align="center"> <p align="center">
@ -21,23 +20,21 @@
<br /> <br />
<a href="https://join.lemmy.ml">Join Lemmy</a> <a href="https://join.lemmy.ml">Join Lemmy</a>
· ·
<a href="https://join.lemmy.ml/docs/en/index.html">Documentation</a> <a href="https://dev.lemmy.ml/docs/index.html">Documentation</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.ml/docs/en/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/static/images/main_img.webp)|![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/static/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).
@ -47,7 +44,7 @@ The overall goal is to create an easily self-hostable, decentralized alternative
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: The WebSocket and HTTP APIs are currently unstable* *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?
@ -68,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.ml/docs/en/administration/install_docker.html) and [Ansible](https://join.lemmy.ml/docs/en/administration/install_ansible.html). - Comes with [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html) and [Ansible](https://dev.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.
@ -103,16 +100,16 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
## Installation ## Installation
- [Docker](https://join.lemmy.ml/docs/en/administration/install_docker.html) - [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
- [Ansible](https://join.lemmy.ml/docs/en/administration/install_ansible.html) - [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
## Lemmy Projects ## Lemmy Projects
### Apps ### Apps
- [lemmy-ui - The official web app for lemmy](https://github.com/LemmyNet/lemmy-ui) - [lemmy-ui - The official web app for lemmy](https://github.com/LemmyNet/lemmy-ui)
- [Lemmur - A mobile client for Lemmy (Android, Linux, Windows)](https://github.com/krawieck/lemmur) - [Lemmur - A flutter lemmy app ( under development )](https://github.com/krawieck/lemmur)
- [Remmel - A native iOS app](https://github.com/uuttff8/Lemmy-iOS) - [Lemmy-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
### Libraries ### Libraries
@ -137,13 +134,13 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
## Contributing ## Contributing
- [Contributing instructions](https://join.lemmy.ml/docs/en/contributing/contributing.html) - [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html)
- [Docker Development](https://join.lemmy.ml/docs/en/contributing/docker_development.html) - [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
- [Local Development](https://join.lemmy.ml/docs/en/contributing/local_development.html) - [Local Development](https://dev.lemmy.ml/docs/contributing_local_development.html)
### Translations ### Translations
If you want to help with translating, take a look at [Weblate](https://weblate.yerbamate.ml/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/).
## Contact ## Contact

View file

@ -1,126 +1,3 @@
# Lemmy v0.9.9 Release (2021-02-19)
## Changes
### Lemmy backend
- Added an federated activity query sorting order.
- Explicitly marking posts and comments as public.
- Added a `NewComment` / forum sort for posts.
- Fixed an issue with not setting correct published time for fetched posts.
- Fixed an issue with an open docker port on lemmy-ui.
- Using lemmy post link for RSS link.
- Fixed reason and display name lengths to use char counts instead.
### Lemmy-ui
- Updated translations.
- Made websocket host configurable.
- Added some accessibility features.
- Always showing password reset link.
# Lemmy v0.9.7 Release (2021-02-08)
## Changes
- Posts and comments are no longer live-sorted (meaning most content should stay in place).
- Fixed an issue with the create post title field not expanding when copied from iframely suggestion.
- Fixed broken federated community paging / sorting.
- Added aria attributes for accessibility, thx to @Mitch Lillie.
- Updated translations and added croatian.
- No changes to lemmy back-end.
# Lemmy v0.9.6 Release (2021-02-05)
## Changes
- Fixed inbox_urls not being correctly set, which broke federation in `v0.9.5`. Added some logging to catch these.
- Fixing community search not using auth.
- Moved docs to https://join.lemmy.ml
- Fixed an issue w/ lemmy-ui with forms being cleared out.
# Lemmy v0.9.4 Pre-Release (2021-02-02)
## Changes
### Lemmy
- Fixed a critical bug with votes and comment unlike responses not being `0` for your user.
- Fixed a critical bug with comment creation not checking if its parent comment is in the post.
- Serving proper activities for community outbox.
- Added some active user counts, including `users_active_day`, `users_active_week`, `users_active_month`, `users_active_half_year` to `SiteAggregates` and `CommunityAggregates`. (Also added to lemmy-ui)
- Made sure banned users can't follow.
- Added `FederatedInstances` to `SiteResponse`, to show allowed and blocked instances. (Also added to lemmy-ui)
- Added a `MostComments` sort for posts. (Also added to lemmy-ui)
### Lemmy-UI
- Added a scroll position restore to lemmy-ui.
- Reworked the combined inbox so incoming comments don't wipe out your current form.
- Fixed an updated bug on the user page.
- Fixed cross-post titles and body getting clipped.
- Fixing the post creation title height.
- Squashed some other smaller bugs.
# Lemmy v0.9.0 Release (2021-01-25)
## Changes
Since our last release in October of last year, and we've had [~450](https://github.com/LemmyNet/lemmy/compare/v0.8.0...main) commits.
The biggest changes, as we'll outline below, are a re-work of Lemmy's database structure, a `v2` of Lemmy's API, and activitypub compliance fixes. The new re-worked DB is much faster, easier to maintain, and [now supports hierarchical rather than flat objects in the new API](https://github.com/LemmyNet/lemmy/issues/1275).
We've also seen the first release of [Lemmur](https://github.com/krawieck/lemmur/releases/tag/v0.1.1), an android / iOS (soon) / windows / linux client, as well as [Lemmer](https://github.com/uuttff8/Lemmy-iOS), a native iOS client. Much thanks to @krawieck, @shilangyu, and @uuttff8 for making these great clients. If you can, please contribute to their [patreon](https://www.patreon.com/lemmur) to help fund lemmur development.
## LemmyNet projects
### Lemmy Server
- [Moved views from SQL to Diesel](https://github.com/LemmyNet/lemmy/issues/1275). This was a spinal replacement for much of lemmy.
- Removed all the old fast_tables and triggers, and created new aggregates tables.
- Added a `v2` of the API to support the hierarchical objects created from the above changes.
- Moved continuous integration to [drone](https://cloud.drone.io/LemmyNet/lemmy/), now includes formatting, clippy, and cargo build checks, unit testing, and federation testing. [Drone also deploys both amd64 and arm64 images to dockerhub.](https://hub.docker.com/r/dessalines/lemmy)
- Split out documentation into git submodule.
- Shortened slur filter to avoid false positives.
- Added query performance testing and comparisons. Added indexes to make sure every query is `< 30 ms`.
- Added compilation time testing.
### Federation
This release includes some bug fixes for federation, and some changes to get us closer to compliance with the ActivityPub standard.
- [Community bans now federating](https://github.com/LemmyNet/lemmy/issues/1287).
- [Local posts sometimes got marked as remote](https://github.com/LemmyNet/lemmy/issues/1302).
- [Creator of post/comment was not notified about new child comments](https://github.com/LemmyNet/lemmy/issues/1325).
- [Community deletion now federated](https://github.com/LemmyNet/lemmy/issues/1256).
None of these are breaking changes, so federation between 0.9.0 and 0.8.11 will work without problems.
### Lemmy javascript / typescript client
- Updated the [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client) to use the new `v2` API. Our API docs now reference this project's files, to show what the http / websocket forms and responses should look like.
- Drone now handles publishing its [npm packages.](https://www.npmjs.com/package/lemmy-js-client)
### Lemmy-UI
- Updated it to use the `v2` API via `lemmy-js-client`, required changing nearly every component.
- Added a live comment count.
- Added drone deploying, and builds for ARM.
- Fixed community link wrapping.
- Various other bug fixes.
### Lemmy Docs
- We moved documentation into a separate git repository, and support translation for the docs now!
- Moved our code of conduct into the documentation.
## Upgrading
If you'd like to make a DB backup before upgrading, follow [this guide](https://join.lemmy.ml/docs/en/administration/backup_and_restore.html).
- [Upgrade with manual Docker installation](https://join.lemmy.ml/docs/en/administration/install_docker.html#updating)
- [Upgrade with Ansible installation](https://join.lemmy.ml/docs/en/administration/install_ansible.html)
# Lemmy v0.8.0 Release (2020-10-16) # Lemmy v0.8.0 Release (2020-10-16)
## Changes ## Changes
@ -143,7 +20,7 @@ Here are some of the bigger changes:
- The first **federation public beta release**, woohoo :fireworks: - The first **federation public beta release**, woohoo :fireworks:
- All Lemmy functionality now works over ActivityPub (except turning remote users into mods/admins) - All Lemmy functionality now works over ActivityPub (except turning remote users into mods/admins)
- Instance allowlist and blocklist - Instance allowlist and blocklist
- Documentation for [admins](https://join.lemmy.ml/docs/administration_federation.html) and [devs](https://join.lemmy.ml/docs/contributing_federation_overview.html) on how federation works - Documentation for [admins](https://dev.lemmy.ml/docs/administration_federation.html) and [devs](https://dev.lemmy.ml/docs/contributing_federation_overview.html) on how federation works
- Upgraded to newest versions of @asonix activitypub libraries - Upgraded to newest versions of @asonix activitypub libraries
- Full local federation setup for manual testing - Full local federation setup for manual testing
- Automated testing for nearly every federation action - Automated testing for nearly every federation action
@ -177,18 +54,18 @@ Here are some of the bigger changes:
## Contributors ## 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. 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://dev.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 ## Upgrading
- [with manual Docker installation](https://join.lemmy.ml/docs/administration_install_docker.html#updating) - [with manual Docker installation](https://dev.lemmy.ml/docs/administration_install_docker.html#updating)
- [with Ansible installation](https://join.lemmy.ml/docs/administration_install_ansible.html) - [with Ansible installation](https://dev.lemmy.ml/docs/administration_install_ansible.html)
## Testing Federation ## 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/) ). 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. If everything goes well, after a few weeks we will enable federation on dev.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. 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.
@ -270,7 +147,7 @@ Overall, since our last major release in January (v0.6.0), we have closed over
Before starting the upgrade, make sure that you have a working backup of your Before starting the upgrade, make sure that you have a working backup of your
database and image files. See our database and image files. See our
[documentation](https://join.lemmy.ml/docs/administration_backup_and_restore.html) [documentation](https://dev.lemmy.ml/docs/administration_backup_and_restore.html)
for backup instructions. for backup instructions.
**With Ansible:** **With Ansible:**
@ -326,4 +203,4 @@ This is the biggest release by far:
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. 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 https://dev.lemmy.ml

View file

@ -1 +1 @@
0.10.0-rc.7 v0.8.2

View file

@ -64,14 +64,6 @@
- src: '../docker/iframely.config.local.js' - src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js' dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600' mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy:dev"
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536"
lemmy_ui_port: "1235"
pictshare_port: "8537"
iframely_port: "8538"
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
- name: add config file (only during initial setup) - name: add config file (only during initial setup)
template: template:

View file

@ -1,6 +1,6 @@
{ {
# for more info about the config, check out the documentation # for more info about the config, check out the documentation
# https://join.lemmy.ml/docs/en/administration/configuration.html # https://dev.lemmy.ml/docs/administration_configuration.html
# settings related to the postgresql database # settings related to the postgresql database
database: { database: {
@ -9,7 +9,7 @@
# host where postgres is running # host where postgres is running
host: "postgres" host: "postgres"
} }
# the domain name of your instance (eg "lemmy.ml") # the domain name of your instance (eg "dev.lemmy.ml")
hostname: "{{ domain }}" hostname: "{{ domain }}"
# json web token for authorization between server and client # json web token for authorization between server and client
jwt_secret: "{{ jwt_password }}" jwt_secret: "{{ jwt_password }}"
@ -26,12 +26,11 @@
# whether to enable activitypub federation. # whether to enable activitypub federation.
enabled: false enabled: false
# Allows and blocks are described here: # Allows and blocks are described here:
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist # https://dev.lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
# #
# comma separated list of instances with which federation is allowed # comma separated list of instances with which federation is allowed
# Only one of these blocks should be uncommented # allowed_instances: ""
# allowed_instances: ["instance1.tld","instance2.tld"]
# comma separated list of instances which are blocked from federating # comma separated list of instances which are blocked from federating
# blocked_instances: [] # blocked_instances: ""
} }
} }

View file

@ -1,4 +1,4 @@
version: '2.2' version: '3.3'
services: services:
lemmy: lemmy:
@ -38,14 +38,13 @@ services:
restart: always restart: always
pictrs: pictrs:
image: asonix/pictrs:v0.2.6-r1 image: asonix/pictrs:v0.2.5-r0
user: 991:991 user: 991:991
ports: ports:
- "127.0.0.1:8537:8080" - "127.0.0.1:8537:8080"
volumes: volumes:
- ./volumes/pictrs:/mnt - ./volumes/pictrs:/mnt
restart: always restart: always
mem_limit: 200m
iframely: iframely:
image: dogbin/iframely:latest image: dogbin/iframely:latest
@ -54,7 +53,6 @@ services:
volumes: volumes:
- ./iframely.config.local.js:/iframely/config.local.js:ro - ./iframely.config.local.js:/iframely/config.local.js:ro
restart: always restart: always
mem_limit: 200m
postfix: postfix:
image: mwader/postfix-relay image: mwader/postfix-relay

View file

@ -61,24 +61,16 @@ server {
if ($http_accept = "application/activity+json") { if ($http_accept = "application/activity+json") {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}"; set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
} }
if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
}
if ($request_method = POST) { if ($request_method = POST) {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}"; set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
} }
proxy_pass $proxpass; proxy_pass $proxpass;
rewrite ^(.+)/+$ $1 permanent; rewrite ^(.+)/+$ $1 permanent;
# Send actual client IP upstream
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# backend # backend
location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known) { location ~ ^/(api|docs|pictrs|feeds|nodeinfo|.well-known) {
proxy_pass http://0.0.0.0:{{ lemmy_port }}; proxy_pass http://0.0.0.0:{{ lemmy_port }};
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;

View file

@ -1,51 +0,0 @@
{
"root": true,
"env": {
"browser": true
},
"plugins": [
"jane"
],
"extends": [
"plugin:jane/recommended",
"plugin:jane/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"warnOnUnsupportedTypeScriptVersion": false
},
"rules": {
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/member-delimiter-style": 0,
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-this-alias": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-useless-constructor": 0,
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,
"eqeqeq": 0,
"func-style": 0,
"import/no-duplicates": 0,
"max-statements": 0,
"max-params": 0,
"new-cap": 0,
"no-console": 0,
"no-duplicate-imports": 0,
"no-extra-parens": 0,
"no-return-assign": 0,
"no-throw-literal": 0,
"no-trailing-spaces": 0,
"no-unused-expressions": 0,
"no-useless-constructor": 0,
"no-useless-escape": 0,
"no-var": 0,
"prefer-const": 0,
"prefer-rest-params": 0,
"quote-props": 0,
"unicorn/filename-case": 0
}
}

View file

@ -1,4 +0,0 @@
module.exports = Object.assign(require('eslint-plugin-jane/prettier-ts'), {
arrowParens: 'avoid',
semi: true,
});

View file

@ -7,19 +7,14 @@
"author": "Dessalines", "author": "Dessalines",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
"fix": "prettier --write src && eslint --fix src",
"api-test": "jest src/ -i --verbose" "api-test": "jest src/ -i --verbose"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.20", "@types/jest": "^26.0.14",
"eslint": "^7.18.0", "jest": "^26.4.2",
"eslint-plugin-jane": "^9.0.3", "lemmy-js-client": "^1.0.14",
"jest": "^26.6.3",
"lemmy-js-client": "0.10.0-rc.4",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"prettier": "^2.1.2", "ts-jest": "^26.4.1",
"ts-jest": "^26.4.4", "typescript": "^4.0.3"
"typescript": "^4.1.3"
} }
} }

View file

@ -1,71 +0,0 @@
#!/bin/bash
set -e
export LEMMY_TEST_SEND_SYNC=1
export RUST_BACKTRACE=1
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
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
killall lemmy_server || true
echo "$PWD"
echo "start alpha"
LEMMY_HOSTNAME=lemmy-alpha:8541 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
LEMMY_HOSTNAME="lemmy-alpha:8541" \
target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
echo "start beta"
LEMMY_HOSTNAME=lemmy-beta:8551 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
echo "start gamma"
LEMMY_HOSTNAME=lemmy-gamma:8561 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
target/lemmy_server >/tmp/lemmy_gamma.out 2>&1 &
echo "start delta"
# An instance with only an allowlist for beta
LEMMY_HOSTNAME=lemmy-delta:8571 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
target/lemmy_server >/tmp/lemmy_delta.out 2>&1 &
echo "start epsilon"
# An instance who has a blocklist, with lemmy-alpha blocked
LEMMY_HOSTNAME=lemmy-epsilon:8581 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
echo "wait for all instances to start"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8541/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8551/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8561/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8571/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8581/api/v2/site')" != "200" ]]; do sleep 1; done

View file

@ -1,20 +0,0 @@
#!/bin/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
./api_tests/prepare-drone-federation-test.sh
popd
yarn
yarn api-test || true
killall lemmy_server
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"
done

View file

@ -11,7 +11,7 @@ import {
followBeta, followBeta,
searchForBetaCommunity, searchForBetaCommunity,
createComment, createComment,
editComment, updateComment,
deleteComment, deleteComment,
removeComment, removeComment,
getMentions, getMentions,
@ -20,8 +20,12 @@ import {
createCommunity, createCommunity,
registerUser, registerUser,
API, API,
delay,
longDelay,
} from './shared'; } from './shared';
import { CommentView } from 'lemmy-js-client'; import {
Comment,
} from 'lemmy-js-client';
import { PostResponse } from 'lemmy-js-client'; import { PostResponse } from 'lemmy-js-client';
@ -32,9 +36,10 @@ beforeAll(async () => {
await followBeta(alpha); await followBeta(alpha);
await followBeta(gamma); await followBeta(gamma);
let search = await searchForBetaCommunity(alpha); let search = await searchForBetaCommunity(alpha);
await longDelay();
postRes = await createPost( postRes = await createPost(
alpha, alpha,
search.communities.find(c => c.community.local == false).community.id search.communities.filter(c => c.local == false)[0].id
); );
}); });
@ -44,34 +49,34 @@ afterAll(async () => {
}); });
function assertCommentFederation( function assertCommentFederation(
commentOne: CommentView, commentOne: Comment,
commentTwo: CommentView commentTwo: Comment) {
) { expect(commentOne.ap_id).toBe(commentOne.ap_id);
expect(commentOne.comment.ap_id).toBe(commentOne.comment.ap_id); expect(commentOne.content).toBe(commentTwo.content);
expect(commentOne.comment.content).toBe(commentTwo.comment.content); expect(commentOne.creator_name).toBe(commentTwo.creator_name);
expect(commentOne.creator.name).toBe(commentTwo.creator.name); expect(commentOne.community_actor_id).toBe(commentTwo.community_actor_id);
expect(commentOne.community.actor_id).toBe(commentTwo.community.actor_id); expect(commentOne.published).toBe(commentTwo.published);
expect(commentOne.comment.published).toBe(commentTwo.comment.published); expect(commentOne.updated).toBe(commentOne.updated);
expect(commentOne.comment.updated).toBe(commentOne.comment.updated); expect(commentOne.deleted).toBe(commentOne.deleted);
expect(commentOne.comment.deleted).toBe(commentOne.comment.deleted); expect(commentOne.removed).toBe(commentOne.removed);
expect(commentOne.comment.removed).toBe(commentOne.comment.removed);
} }
test('Create a comment', async () => { test('Create a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
expect(commentRes.comment_view.comment.content).toBeDefined(); expect(commentRes.comment.content).toBeDefined();
expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment.community_local).toBe(false);
expect(commentRes.comment_view.creator.local).toBe(true); expect(commentRes.comment.creator_local).toBe(true);
expect(commentRes.comment_view.counts.score).toBe(1); expect(commentRes.comment.score).toBe(1);
await longDelay();
// Make sure that comment is liked on beta // Make sure that comment is liked on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
expect(betaComment).toBeDefined(); expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true); expect(betaComment.community_local).toBe(true);
expect(betaComment.creator.local).toBe(false); expect(betaComment.creator_local).toBe(false);
expect(betaComment.counts.score).toBe(1); expect(betaComment.score).toBe(1);
assertCommentFederation(betaComment, commentRes.comment_view); assertCommentFederation(betaComment, commentRes.comment);
}); });
test('Create a comment in a non-existent post', async () => { test('Create a comment in a non-existent post', async () => {
@ -80,90 +85,83 @@ test('Create a comment in a non-existent post', async () => {
}); });
test('Update a comment', async () => { test('Update a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
// Federate the comment first // Federate the comment first
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
assertCommentFederation(searchBeta.comments[0], commentRes.comment_view); assertCommentFederation(searchBeta.comments[0], commentRes.comment);
let updateCommentRes = await editComment( await delay();
alpha, let updateCommentRes = await updateComment(alpha, commentRes.comment.id);
commentRes.comment_view.comment.id expect(updateCommentRes.comment.content).toBe(
);
expect(updateCommentRes.comment_view.comment.content).toBe(
'A jest test federated comment update' 'A jest test federated comment update'
); );
expect(updateCommentRes.comment_view.community.local).toBe(false); expect(updateCommentRes.comment.community_local).toBe(false);
expect(updateCommentRes.comment_view.creator.local).toBe(true); expect(updateCommentRes.comment.creator_local).toBe(true);
await delay();
// Make sure that post is updated on beta // Make sure that post is updated on beta
let searchBetaUpdated = await searchComment( let searchBetaUpdated = await searchComment(beta, commentRes.comment);
beta, assertCommentFederation(searchBetaUpdated.comments[0], updateCommentRes.comment);
commentRes.comment_view.comment
);
assertCommentFederation(
searchBetaUpdated.comments[0],
updateCommentRes.comment_view
);
}); });
test('Delete a comment', async () => { test('Delete a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
await delay();
let deleteCommentRes = await deleteComment( let deleteCommentRes = await deleteComment(
alpha, alpha,
true, true,
commentRes.comment_view.comment.id commentRes.comment.id
); );
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true); expect(deleteCommentRes.comment.deleted).toBe(true);
await delay();
// Make sure that comment is undefined on beta // Make sure that comment is undefined on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
expect(betaComment).toBeUndefined(); expect(betaComment).toBeUndefined();
await delay();
let undeleteCommentRes = await deleteComment( let undeleteCommentRes = await deleteComment(
alpha, alpha,
false, false,
commentRes.comment_view.comment.id commentRes.comment.id
); );
expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false); expect(undeleteCommentRes.comment.deleted).toBe(false);
await delay();
// Make sure that comment is undeleted on beta // Make sure that comment is undeleted on beta
let searchBeta2 = await searchComment(beta, commentRes.comment_view.comment); let searchBeta2 = await searchComment(beta, commentRes.comment);
let betaComment2 = searchBeta2.comments[0]; let betaComment2 = searchBeta2.comments[0];
expect(betaComment2.comment.deleted).toBe(false); expect(betaComment2.deleted).toBe(false);
assertCommentFederation( assertCommentFederation(searchBeta2.comments[0], undeleteCommentRes.comment);
searchBeta2.comments[0],
undeleteCommentRes.comment_view
);
}); });
test('Remove a comment from admin and community on the same instance', async () => { test('Remove a comment from admin and community on the same instance', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
await delay();
// Get the id for beta // Get the id for beta
let betaCommentId = ( let betaCommentId = (await searchComment(beta, commentRes.comment))
await searchComment(beta, commentRes.comment_view.comment) .comments[0].id;
).comments[0].comment.id;
// The beta admin removes it (the community lives on beta) // The beta admin removes it (the community lives on beta)
let removeCommentRes = await removeComment(beta, true, betaCommentId); let removeCommentRes = await removeComment(beta, true, betaCommentId);
expect(removeCommentRes.comment_view.comment.removed).toBe(true); expect(removeCommentRes.comment.removed).toBe(true);
await longDelay();
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it) // Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPost = await getPost(alpha, postRes.post_view.post.id); let refetchedPost = await getPost(alpha, postRes.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(true); expect(refetchedPost.comments[0].removed).toBe(true);
let unremoveCommentRes = await removeComment(beta, false, betaCommentId); let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
expect(unremoveCommentRes.comment_view.comment.removed).toBe(false); expect(unremoveCommentRes.comment.removed).toBe(false);
await longDelay();
// Make sure that comment is unremoved on beta // Make sure that comment is unremoved on beta
let refetchedPost2 = await getPost(alpha, postRes.post_view.post.id); let refetchedPost2 = await getPost(alpha, postRes.post.id);
expect(refetchedPost2.comments[0].comment.removed).toBe(false); expect(refetchedPost2.comments[0].removed).toBe(false);
assertCommentFederation( assertCommentFederation(refetchedPost2.comments[0], unremoveCommentRes.comment);
refetchedPost2.comments[0],
unremoveCommentRes.comment_view
);
}); });
test('Remove a comment from admin and community on different instance', async () => { test('Remove a comment from admin and community on different instance', async () => {
@ -175,155 +173,160 @@ test('Remove a comment from admin and community on different instance', async ()
// New alpha user creates a community, post, and comment. // New alpha user creates a community, post, and comment.
let newCommunity = await createCommunity(newAlphaApi); let newCommunity = await createCommunity(newAlphaApi);
let newPost = await createPost( await delay();
newAlphaApi, let newPost = await createPost(newAlphaApi, newCommunity.community.id);
newCommunity.community_view.community.id await delay();
); let commentRes = await createComment(newAlphaApi, newPost.post.id);
let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id); expect(commentRes.comment.content).toBeDefined();
expect(commentRes.comment_view.comment.content).toBeDefined(); await delay();
// Beta searches that to cache it, then removes it // Beta searches that to cache it, then removes it
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
let removeCommentRes = await removeComment( let removeCommentRes = await removeComment(beta, true, betaComment.id);
beta, expect(removeCommentRes.comment.removed).toBe(true);
true, await delay();
betaComment.comment.id
);
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
// Make sure its not removed on alpha // Make sure its not removed on alpha
let refetchedPost = await getPost(newAlphaApi, newPost.post_view.post.id); let refetchedPost = await getPost(newAlphaApi, newPost.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(false); expect(refetchedPost.comments[0].removed).toBe(false);
assertCommentFederation(refetchedPost.comments[0], commentRes.comment_view); assertCommentFederation(refetchedPost.comments[0], commentRes.comment);
}); });
test('Unlike a comment', async () => { test('Unlike a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment); await delay();
expect(unlike.comment_view.counts.score).toBe(0); let unlike = await likeComment(alpha, 0, commentRes.comment);
expect(unlike.comment.score).toBe(0);
await delay();
// Make sure that post is unliked on beta // Make sure that post is unliked on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
expect(betaComment).toBeDefined(); expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true); expect(betaComment.community_local).toBe(true);
expect(betaComment.creator.local).toBe(false); expect(betaComment.creator_local).toBe(false);
expect(betaComment.counts.score).toBe(0); expect(betaComment.score).toBe(0);
}); });
test('Federated comment like', async () => { test('Federated comment like', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
await longDelay();
// Find the comment on beta // Find the comment on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
let like = await likeComment(beta, 1, betaComment.comment); let like = await likeComment(beta, 1, betaComment);
expect(like.comment_view.counts.score).toBe(2); expect(like.comment.score).toBe(2);
await longDelay();
// Get the post from alpha, check the likes // Get the post from alpha, check the likes
let post = await getPost(alpha, postRes.post_view.post.id); let post = await getPost(alpha, postRes.post.id);
expect(post.comments[0].counts.score).toBe(2); expect(post.comments[0].score).toBe(2);
}); });
test('Reply to a comment', async () => { test('Reply to a comment', async () => {
// Create a comment on alpha, find it on beta // Create a comment on alpha, find it on beta
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); await delay();
let searchBeta = await searchComment(beta, commentRes.comment);
let betaComment = searchBeta.comments[0]; let betaComment = searchBeta.comments[0];
// find that comment id on beta // find that comment id on beta
// Reply from beta // Reply from beta
let replyRes = await createComment( let replyRes = await createComment(beta, betaComment.post_id, betaComment.id);
beta, expect(replyRes.comment.content).toBeDefined();
betaComment.post.id, expect(replyRes.comment.community_local).toBe(true);
betaComment.comment.id expect(replyRes.comment.creator_local).toBe(true);
); expect(replyRes.comment.parent_id).toBe(betaComment.id);
expect(replyRes.comment_view.comment.content).toBeDefined(); expect(replyRes.comment.score).toBe(1);
expect(replyRes.comment_view.community.local).toBe(true); await longDelay();
expect(replyRes.comment_view.creator.local).toBe(true);
expect(replyRes.comment_view.comment.parent_id).toBe(betaComment.comment.id);
expect(replyRes.comment_view.counts.score).toBe(1);
// Make sure that comment is seen on alpha // Make sure that comment is seen on alpha
// TODO not sure why, but a searchComment back to alpha, for the ap_id of betas // TODO not sure why, but a searchComment back to alpha, for the ap_id of betas
// comment, isn't working. // comment, isn't working.
// let searchAlpha = await searchComment(alpha, replyRes.comment); // let searchAlpha = await searchComment(alpha, replyRes.comment);
let post = await getPost(alpha, postRes.post_view.post.id); let post = await getPost(alpha, postRes.post.id);
let alphaComment = post.comments[0]; let alphaComment = post.comments[0];
expect(alphaComment.comment.content).toBeDefined(); expect(alphaComment.content).toBeDefined();
expect(alphaComment.comment.parent_id).toBe(post.comments[1].comment.id); expect(alphaComment.parent_id).toBe(post.comments[1].id);
expect(alphaComment.community.local).toBe(false); expect(alphaComment.community_local).toBe(false);
expect(alphaComment.creator.local).toBe(false); expect(alphaComment.creator_local).toBe(false);
expect(alphaComment.counts.score).toBe(1); expect(alphaComment.score).toBe(1);
assertCommentFederation(alphaComment, replyRes.comment_view); assertCommentFederation(alphaComment, replyRes.comment);
}); });
test('Mention beta', async () => { test('Mention beta', async () => {
// Create a mention on alpha // Create a mention on alpha
let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8551'; let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
await delay();
let mentionRes = await createComment( let mentionRes = await createComment(
alpha, alpha,
postRes.post_view.post.id, postRes.post.id,
commentRes.comment_view.comment.id, commentRes.comment.id,
mentionContent mentionContent
); );
expect(mentionRes.comment_view.comment.content).toBeDefined(); expect(mentionRes.comment.content).toBeDefined();
expect(mentionRes.comment_view.community.local).toBe(false); expect(mentionRes.comment.community_local).toBe(false);
expect(mentionRes.comment_view.creator.local).toBe(true); expect(mentionRes.comment.creator_local).toBe(true);
expect(mentionRes.comment_view.counts.score).toBe(1); expect(mentionRes.comment.score).toBe(1);
await delay();
let mentionsRes = await getMentions(beta); let mentionsRes = await getMentions(beta);
expect(mentionsRes.mentions[0].comment.content).toBeDefined(); expect(mentionsRes.mentions[0].content).toBeDefined();
expect(mentionsRes.mentions[0].community.local).toBe(true); expect(mentionsRes.mentions[0].community_local).toBe(true);
expect(mentionsRes.mentions[0].creator.local).toBe(false); expect(mentionsRes.mentions[0].creator_local).toBe(false);
expect(mentionsRes.mentions[0].counts.score).toBe(1); expect(mentionsRes.mentions[0].score).toBe(1);
}); });
test('Comment Search', async () => { test('Comment Search', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post.id);
let searchBeta = await searchComment(beta, commentRes.comment_view.comment); await delay();
assertCommentFederation(searchBeta.comments[0], commentRes.comment_view); let searchBeta = await searchComment(beta, commentRes.comment);
assertCommentFederation(searchBeta.comments[0], commentRes.comment);
}); });
test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => { test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => {
// Create a local post // Create a local post
let alphaPost = await createPost(alpha, 2); let alphaPost = await createPost(alpha, 2);
expect(alphaPost.post_view.community.local).toBe(true); expect(alphaPost.post.community_local).toBe(true);
await delay();
// Make sure gamma sees it // Make sure gamma sees it
let search = await searchPost(gamma, alphaPost.post_view.post); let search = await searchPost(gamma, alphaPost.post);
let gammaPost = search.posts[0]; let gammaPost = search.posts[0];
let commentContent = let commentContent =
'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551'; 'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment( let commentRes = await createComment(
gamma, gamma,
gammaPost.post.id, gammaPost.id,
undefined, undefined,
commentContent commentContent
); );
expect(commentRes.comment_view.comment.content).toBe(commentContent); expect(commentRes.comment.content).toBe(commentContent);
expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment.community_local).toBe(false);
expect(commentRes.comment_view.creator.local).toBe(true); expect(commentRes.comment.creator_local).toBe(true);
expect(commentRes.comment_view.counts.score).toBe(1); expect(commentRes.comment.score).toBe(1);
await longDelay();
// Make sure alpha sees it // Make sure alpha sees it
let alphaPost2 = await getPost(alpha, alphaPost.post_view.post.id); let alphaPost2 = await getPost(alpha, alphaPost.post.id);
expect(alphaPost2.comments[0].comment.content).toBe(commentContent); expect(alphaPost2.comments[0].content).toBe(commentContent);
expect(alphaPost2.comments[0].community.local).toBe(true); expect(alphaPost2.comments[0].community_local).toBe(true);
expect(alphaPost2.comments[0].creator.local).toBe(false); expect(alphaPost2.comments[0].creator_local).toBe(false);
expect(alphaPost2.comments[0].counts.score).toBe(1); expect(alphaPost2.comments[0].score).toBe(1);
assertCommentFederation(alphaPost2.comments[0], commentRes.comment_view); assertCommentFederation(alphaPost2.comments[0], commentRes.comment);
await delay();
// Make sure beta has mentions // Make sure beta has mentions
let mentionsRes = await getMentions(beta); let mentionsRes = await getMentions(beta);
expect(mentionsRes.mentions[0].comment.content).toBe(commentContent); expect(mentionsRes.mentions[0].content).toBe(commentContent);
expect(mentionsRes.mentions[0].community.local).toBe(false); expect(mentionsRes.mentions[0].community_local).toBe(false);
expect(mentionsRes.mentions[0].creator.local).toBe(false); expect(mentionsRes.mentions[0].creator_local).toBe(false);
// TODO this is failing because fetchInReplyTos aren't getting score // TODO this is failing because fetchInReplyTos aren't getting score
// expect(mentionsRes.mentions[0].score).toBe(1); // expect(mentionsRes.mentions[0].score).toBe(1);
}); });
@ -332,60 +335,60 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
// Unfollow all remote communities // Unfollow all remote communities
let followed = await unfollowRemotes(alpha); let followed = await unfollowRemotes(alpha);
expect( expect(
followed.communities.filter(c => c.community.local == false).length followed.communities.filter(c => c.community_local == false).length
).toBe(0); ).toBe(0);
// B creates a post, and two comments, should be invisible to A // B creates a post, and two comments, should be invisible to A
let postRes = await createPost(beta, 2); let postRes = await createPost(beta, 2);
expect(postRes.post_view.post.name).toBeDefined(); expect(postRes.post.name).toBeDefined();
await delay();
let parentCommentContent = 'An invisible top level comment from beta'; let parentCommentContent = 'An invisible top level comment from beta';
let parentCommentRes = await createComment( let parentCommentRes = await createComment(
beta, beta,
postRes.post_view.post.id, postRes.post.id,
undefined, undefined,
parentCommentContent parentCommentContent
); );
expect(parentCommentRes.comment_view.comment.content).toBe( expect(parentCommentRes.comment.content).toBe(parentCommentContent);
parentCommentContent await delay();
);
// B creates a comment, then a child one of that. // B creates a comment, then a child one of that.
let childCommentContent = 'An invisible child comment from beta'; let childCommentContent = 'An invisible child comment from beta';
let childCommentRes = await createComment( let childCommentRes = await createComment(
beta, beta,
postRes.post_view.post.id, postRes.post.id,
parentCommentRes.comment_view.comment.id, parentCommentRes.comment.id,
childCommentContent
);
expect(childCommentRes.comment_view.comment.content).toBe(
childCommentContent childCommentContent
); );
expect(childCommentRes.comment.content).toBe(childCommentContent);
await delay();
// Follow beta again // Follow beta again
let follow = await followBeta(alpha); let follow = await followBeta(alpha);
expect(follow.community_view.community.local).toBe(false); expect(follow.community.local).toBe(false);
expect(follow.community_view.community.name).toBe('main'); expect(follow.community.name).toBe('main');
await delay();
// An update to the child comment on beta, should push the post, parent, and child to alpha now // An update to the child comment on beta, should push the post, parent, and child to alpha now
let updatedCommentContent = 'An update child comment from beta'; let updatedCommentContent = 'An update child comment from beta';
let updateRes = await editComment( let updateRes = await updateComment(
beta, beta,
childCommentRes.comment_view.comment.id, childCommentRes.comment.id,
updatedCommentContent updatedCommentContent
); );
expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent); expect(updateRes.comment.content).toBe(updatedCommentContent);
await delay();
// Get the post from alpha // Get the post from alpha
let search = await searchPost(alpha, postRes.post_view.post); let search = await searchPost(alpha, postRes.post);
let alphaPostB = search.posts[0]; let alphaPostB = search.posts[0];
await longDelay();
let alphaPost = await getPost(alpha, alphaPostB.post.id); let alphaPost = await getPost(alpha, alphaPostB.id);
expect(alphaPost.post_view.post.name).toBeDefined(); expect(alphaPost.post.name).toBeDefined();
assertCommentFederation(alphaPost.comments[1], parentCommentRes.comment_view); assertCommentFederation(alphaPost.comments[1], parentCommentRes.comment);
assertCommentFederation(alphaPost.comments[0], updateRes.comment_view); assertCommentFederation(alphaPost.comments[0], updateRes.comment);
expect(alphaPost.post_view.community.local).toBe(false); expect(alphaPost.post.community_local).toBe(false);
expect(alphaPost.post_view.creator.local).toBe(false); expect(alphaPost.post.creator_local).toBe(false);
await unfollowRemotes(alpha);
}); });

View file

@ -3,164 +3,155 @@ import {
alpha, alpha,
beta, beta,
setupLogins, setupLogins,
searchForBetaCommunity,
searchForCommunity, searchForCommunity,
createCommunity, createCommunity,
deleteCommunity, deleteCommunity,
removeCommunity, removeCommunity,
getCommunity, getCommunity,
followCommunity, followCommunity,
delay,
longDelay,
} from './shared'; } from './shared';
import { CommunityView } from 'lemmy-js-client'; import {
Community,
} from 'lemmy-js-client';
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); 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(communityTwo.community.actor_id); expect(communityOne.name).toBe(communityTwo.name);
expect(communityOne.community.name).toBe(communityTwo.community.name); expect(communityOne.title).toBe(communityTwo.title);
expect(communityOne.community.title).toBe(communityTwo.community.title); expect(communityOne.description).toBe(communityTwo.description);
expect(communityOne.community.description).toBe( expect(communityOne.icon).toBe(communityTwo.icon);
communityTwo.community.description expect(communityOne.banner).toBe(communityTwo.banner);
); expect(communityOne.published).toBe(communityTwo.published);
expect(communityOne.community.icon).toBe(communityTwo.community.icon); expect(communityOne.creator_actor_id).toBe(communityTwo.creator_actor_id);
expect(communityOne.community.banner).toBe(communityTwo.community.banner); expect(communityOne.nsfw).toBe(communityTwo.nsfw);
expect(communityOne.community.published).toBe( expect(communityOne.category_id).toBe(communityTwo.category_id);
communityTwo.community.published expect(communityOne.removed).toBe(communityTwo.removed);
); expect(communityOne.deleted).toBe(communityTwo.deleted);
expect(communityOne.creator.actor_id).toBe(communityTwo.creator.actor_id);
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;
let communityRes2: any = await createCommunity(alpha, prevName); let communityRes2 = await createCommunity(alpha, prevName);
expect(communityRes2['error']).toBe('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 search = await searchForCommunity(beta, searchShort); let search = await searchForCommunity(beta, searchShort);
let communityOnBeta = search.communities[0]; let communityOnBeta = search.communities[0];
assertCommunityFederation(communityOnBeta, communityRes.community_view); 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 search = await searchForCommunity(alpha, searchShort); let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0]; let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view); assertCommunityFederation(communityOnAlpha, communityRes.community);
await delay();
// Follow the community from alpha // Follow the community from alpha
let follow = await followCommunity( let follow = await followCommunity(alpha, true, communityOnAlpha.id);
alpha,
true,
communityOnAlpha.community.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.deleted).toBe(true);
await delay();
// Make sure it got deleted on A // Make sure it got deleted on A
let communityOnAlphaDeleted = await getCommunity( let communityOnAlphaDeleted = await getCommunity(alpha, communityOnAlpha.id);
alpha, expect(communityOnAlphaDeleted.community.deleted).toBe(true);
communityOnAlpha.community.id 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 getCommunity( let communityOnAlphaUnDeleted = await getCommunity(alpha, communityOnAlpha.id);
alpha, expect(communityOnAlphaUnDeleted.community.deleted).toBe(false);
communityOnAlpha.community.id
);
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 search = await searchForCommunity(alpha, searchShort); let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0]; let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view); assertCommunityFederation(communityOnAlpha, communityRes.community);
await delay();
// Follow the community from alpha // Follow the community from alpha
let follow = await followCommunity( let follow = await followCommunity(alpha, true, communityOnAlpha.id);
alpha,
true,
communityOnAlpha.community.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.removed).toBe(true);
await delay();
// Make sure it got Removed on A // Make sure it got Removed on A
let communityOnAlphaRemoved = await getCommunity( let communityOnAlphaRemoved = await getCommunity(alpha, communityOnAlpha.id);
alpha, expect(communityOnAlphaRemoved.community.removed).toBe(true);
communityOnAlpha.community.id 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 getCommunity( let communityOnAlphaUnRemoved = await getCommunity(alpha, communityOnAlpha.id);
alpha, expect(communityOnAlphaUnRemoved.community.removed).toBe(false);
communityOnAlpha.community.id
);
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(); expect(communityRes.community.name).toBeDefined();
await delay();
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; let searchShort = `!${communityRes.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort); let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0]; let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view); assertCommunityFederation(communityOnAlpha, communityRes.community);
}); });

View file

@ -6,6 +6,8 @@ import {
followCommunity, followCommunity,
checkFollowedCommunities, checkFollowedCommunities,
unfollowRemotes, unfollowRemotes,
delay,
longDelay,
} from './shared'; } from './shared';
beforeAll(async () => { beforeAll(async () => {
@ -18,26 +20,25 @@ afterAll(async () => {
test('Follow federated community', async () => { test('Follow federated community', async () => {
let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null? let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
let follow = await followCommunity( let follow = await followCommunity(alpha, true, search.communities[0].id);
alpha,
true,
search.communities[0].community.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);
expect(follow.community_view.community.name).toBe('main'); expect(follow.community.name).toBe('main');
await longDelay();
// Check it from local // Check it from local
let followCheck = await checkFollowedCommunities(alpha); let followCheck = await checkFollowedCommunities(alpha);
let remoteCommunityId = followCheck.communities.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();
// Test an unfollow // Test an unfollow
let unfollow = await followCommunity(alpha, false, remoteCommunityId); let unfollow = await followCommunity(alpha, false, remoteCommunityId);
expect(unfollow.community_view.community.local).toBe(false); expect(unfollow.community.local).toBe(false);
await delay();
// Make sure you are unsubbed locally // Make sure you are unsubbed locally
let unfollowCheck = await checkFollowedCommunities(alpha); let unfollowCheck = await checkFollowedCommunities(alpha);

View file

@ -7,7 +7,7 @@ import {
epsilon, epsilon,
setupLogins, setupLogins,
createPost, createPost,
editPost, updatePost,
stickyPost, stickyPost,
lockPost, lockPost,
searchPost, searchPost,
@ -19,72 +19,73 @@ import {
removePost, removePost,
getPost, getPost,
unfollowRemotes, unfollowRemotes,
searchForUser, delay,
banPersonFromSite, longDelay,
searchPostLocal,
banPersonFromCommunity,
} from './shared'; } from './shared';
import { PostView, CommunityView } from 'lemmy-js-client'; import {
Post,
let betaCommunity: CommunityView; } from 'lemmy-js-client';
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
let search = await searchForBetaCommunity(alpha); await followBeta(alpha);
betaCommunity = search.communities[0]; await followBeta(gamma);
await unfollows(); await followBeta(delta);
await followBeta(epsilon);
await longDelay();
}); });
afterAll(async () => { afterAll(async () => {
await unfollows();
});
async function unfollows() {
await unfollowRemotes(alpha); await unfollowRemotes(alpha);
await unfollowRemotes(gamma); await unfollowRemotes(gamma);
await unfollowRemotes(delta); await unfollowRemotes(delta);
await unfollowRemotes(epsilon); await unfollowRemotes(epsilon);
} });
function assertPostFederation(postOne: PostView, postTwo: PostView) { function assertPostFederation(
expect(postOne.post.ap_id).toBe(postTwo.post.ap_id); postOne: Post,
expect(postOne.post.name).toBe(postTwo.post.name); postTwo: Post) {
expect(postOne.post.body).toBe(postTwo.post.body); expect(postOne.ap_id).toBe(postTwo.ap_id);
expect(postOne.post.url).toBe(postTwo.post.url); expect(postOne.name).toBe(postTwo.name);
expect(postOne.post.nsfw).toBe(postTwo.post.nsfw); expect(postOne.body).toBe(postTwo.body);
expect(postOne.post.embed_title).toBe(postTwo.post.embed_title); expect(postOne.url).toBe(postTwo.url);
expect(postOne.post.embed_description).toBe(postTwo.post.embed_description); expect(postOne.nsfw).toBe(postTwo.nsfw);
expect(postOne.post.embed_html).toBe(postTwo.post.embed_html); expect(postOne.embed_title).toBe(postTwo.embed_title);
expect(postOne.post.published).toBe(postTwo.post.published); expect(postOne.embed_description).toBe(postTwo.embed_description);
expect(postOne.community.actor_id).toBe(postTwo.community.actor_id); expect(postOne.embed_html).toBe(postTwo.embed_html);
expect(postOne.post.locked).toBe(postTwo.post.locked); expect(postOne.published).toBe(postTwo.published);
expect(postOne.post.removed).toBe(postTwo.post.removed); expect(postOne.community_actor_id).toBe(postTwo.community_actor_id);
expect(postOne.post.deleted).toBe(postTwo.post.deleted); expect(postOne.locked).toBe(postTwo.locked);
expect(postOne.removed).toBe(postTwo.removed);
expect(postOne.deleted).toBe(postTwo.deleted);
} }
test('Create a post', async () => { test('Create a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
expect(postRes.post_view.post).toBeDefined(); await delay();
expect(postRes.post_view.community.local).toBe(false); let postRes = await createPost(alpha, search.communities[0].id);
expect(postRes.post_view.creator.local).toBe(true); expect(postRes.post).toBeDefined();
expect(postRes.post_view.counts.score).toBe(1); expect(postRes.post.community_local).toBe(false);
expect(postRes.post.creator_local).toBe(true);
expect(postRes.post.score).toBe(1);
await longDelay();
// Make sure that post is liked on beta // Make sure that post is liked on beta
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined(); expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true); expect(betaPost.community_local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator_local).toBe(false);
expect(betaPost.counts.score).toBe(1); expect(betaPost.score).toBe(1);
assertPostFederation(betaPost, postRes.post_view); assertPostFederation(betaPost, postRes.post);
// Delta only follows beta, so it should not see an alpha ap_id // Delta only follows beta, so it should not see an alpha ap_id
let searchDelta = await searchPost(delta, postRes.post_view.post); let searchDelta = await searchPost(delta, postRes.post);
expect(searchDelta.posts[0]).toBeUndefined(); expect(searchDelta.posts[0]).toBeUndefined();
// Epsilon has alpha blocked, it should not see the alpha post // Epsilon has alpha blocked, it should not see the alpha post
let searchEpsilon = await searchPost(epsilon, postRes.post_view.post); let searchEpsilon = await searchPost(epsilon, postRes.post);
expect(searchEpsilon.posts[0]).toBeUndefined(); expect(searchEpsilon.posts[0]).toBeUndefined();
}); });
@ -94,266 +95,241 @@ test('Create a post in a non-existent community', async () => {
}); });
test('Unlike a post', async () => { test('Unlike a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
let unlike = await likePost(alpha, 0, postRes.post_view.post); let postRes = await createPost(alpha, search.communities[0].id);
expect(unlike.post_view.counts.score).toBe(0); await delay();
let unlike = await likePost(alpha, 0, postRes.post);
expect(unlike.post.score).toBe(0);
await delay();
// Try to unlike it again, make sure it stays at 0 // Try to unlike it again, make sure it stays at 0
let unlike2 = await likePost(alpha, 0, postRes.post_view.post); let unlike2 = await likePost(alpha, 0, postRes.post);
expect(unlike2.post_view.counts.score).toBe(0); expect(unlike2.post.score).toBe(0);
await longDelay();
// Make sure that post is unliked on beta // Make sure that post is unliked on beta
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined(); expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true); expect(betaPost.community_local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator_local).toBe(false);
expect(betaPost.counts.score).toBe(0); expect(betaPost.score).toBe(0);
assertPostFederation(betaPost, postRes.post_view); assertPostFederation(betaPost, postRes.post);
}); });
test('Update a post', async () => { test('Update a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let updatedName = 'A jest test federated post, updated'; let updatedName = 'A jest test federated post, updated';
let updatedPost = await editPost(alpha, postRes.post_view.post); let updatedPost = await updatePost(alpha, postRes.post);
expect(updatedPost.post_view.post.name).toBe(updatedName); expect(updatedPost.post.name).toBe(updatedName);
expect(updatedPost.post_view.community.local).toBe(false); expect(updatedPost.post.community_local).toBe(false);
expect(updatedPost.post_view.creator.local).toBe(true); expect(updatedPost.post.creator_local).toBe(true);
await delay();
// Make sure that post is updated on beta // Make sure that post is updated on beta
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost.community.local).toBe(true); expect(betaPost.community_local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator_local).toBe(false);
expect(betaPost.post.name).toBe(updatedName); expect(betaPost.name).toBe(updatedName);
assertPostFederation(betaPost, updatedPost.post_view); assertPostFederation(betaPost, updatedPost.post);
await delay();
// Make sure lemmy beta cannot update the post // Make sure lemmy beta cannot update the post
let updatedPostBeta = await editPost(beta, betaPost.post); let updatedPostBeta = await updatePost(beta, betaPost);
expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
}); });
test('Sticky a post', async () => { test('Sticky a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let stickiedPostRes = await stickyPost(alpha, true, postRes.post_view.post); let stickiedPostRes = await stickyPost(alpha, true, postRes.post);
expect(stickiedPostRes.post_view.post.stickied).toBe(true); expect(stickiedPostRes.post.stickied).toBe(true);
await delay();
// Make sure that post is stickied on beta // Make sure that post is stickied on beta
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost.community.local).toBe(true); expect(betaPost.community_local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator_local).toBe(false);
expect(betaPost.post.stickied).toBe(true); expect(betaPost.stickied).toBe(true);
// Unsticky a post // Unsticky a post
let unstickiedPost = await stickyPost(alpha, false, postRes.post_view.post); let unstickiedPost = await stickyPost(alpha, false, postRes.post);
expect(unstickiedPost.post_view.post.stickied).toBe(false); expect(unstickiedPost.post.stickied).toBe(false);
await delay();
// Make sure that post is unstickied on beta // Make sure that post is unstickied on beta
let searchBeta2 = await searchPost(beta, postRes.post_view.post); let searchBeta2 = await searchPost(beta, postRes.post);
let betaPost2 = searchBeta2.posts[0]; let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.community.local).toBe(true); expect(betaPost2.community_local).toBe(true);
expect(betaPost2.creator.local).toBe(false); expect(betaPost2.creator_local).toBe(false);
expect(betaPost2.post.stickied).toBe(false); expect(betaPost2.stickied).toBe(false);
// Make sure that gamma cannot sticky the post on beta // Make sure that gamma cannot sticky the post on beta
let searchGamma = await searchPost(gamma, postRes.post_view.post); let searchGamma = await searchPost(gamma, postRes.post);
let gammaPost = searchGamma.posts[0]; let gammaPost = searchGamma.posts[0];
let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post); let gammaTrySticky = await stickyPost(gamma, true, gammaPost);
let searchBeta3 = await searchPost(beta, postRes.post_view.post); await delay();
let searchBeta3 = await searchPost(beta, postRes.post);
let betaPost3 = searchBeta3.posts[0]; let betaPost3 = searchBeta3.posts[0];
expect(gammaTrySticky.post_view.post.stickied).toBe(true); expect(gammaTrySticky.post.stickied).toBe(true);
expect(betaPost3.post.stickied).toBe(false); expect(betaPost3.stickied).toBe(false);
}); });
test('Lock a post', async () => { test('Lock a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
await delay();
let postRes = await createPost(alpha, search.communities[0].id);
await delay();
// Lock the post let lockedPostRes = await lockPost(alpha, true, postRes.post);
let lockedPostRes = await lockPost(alpha, true, postRes.post_view.post); expect(lockedPostRes.post.locked).toBe(true);
expect(lockedPostRes.post_view.post.locked).toBe(true); await delay();
// Make sure that post is locked on beta // Make sure that post is locked on beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost1 = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost1.post.locked).toBe(true); expect(betaPost.community_local).toBe(true);
expect(betaPost.creator_local).toBe(false);
expect(betaPost.locked).toBe(true);
// Try to make a new comment there, on alpha // Try to make a new comment there, on alpha
let comment: any = await createComment(alpha, postRes.post_view.post.id); let comment = await createComment(alpha, postRes.post.id);
expect(comment['error']).toBe('locked'); expect(comment['error']).toBe('locked');
await delay();
// Unlock a post
let unlockedPost = await lockPost(alpha, false, postRes.post_view.post);
expect(unlockedPost.post_view.post.locked).toBe(false);
// Make sure that post is unlocked on beta
let searchBeta2 = await searchPost(beta, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.community.local).toBe(true);
expect(betaPost2.creator.local).toBe(false);
expect(betaPost2.post.locked).toBe(false);
// Try to create a new comment, on beta // Try to create a new comment, on beta
let commentBeta = await createComment(beta, betaPost2.post.id); let commentBeta = await createComment(beta, betaPost.id);
expect(commentBeta).toBeDefined(); expect(commentBeta['error']).toBe('locked');
await delay();
// Unlock a post
let unlockedPost = await lockPost(alpha, false, postRes.post);
expect(unlockedPost.post.locked).toBe(false);
await delay();
// Make sure that post is unlocked on beta
let searchBeta2 = await searchPost(beta, postRes.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.community_local).toBe(true);
expect(betaPost2.creator_local).toBe(false);
expect(betaPost2.locked).toBe(false);
}); });
test('Delete a post', async () => { test('Delete a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
expect(postRes.post_view.post).toBeDefined(); let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let deletedPost = await deletePost(alpha, true, postRes.post_view.post); let deletedPost = await deletePost(alpha, true, postRes.post);
expect(deletedPost.post_view.post.deleted).toBe(true); expect(deletedPost.post.deleted).toBe(true);
await delay();
// Make sure lemmy beta sees post is deleted // Make sure lemmy beta sees post is deleted
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
// This will be undefined because of the tombstone // This will be undefined because of the tombstone
expect(betaPost).toBeUndefined(); expect(betaPost).toBeUndefined();
await delay();
// Undelete // Undelete
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post); let undeletedPost = await deletePost(alpha, false, postRes.post);
expect(undeletedPost.post_view.post.deleted).toBe(false); expect(undeletedPost.post.deleted).toBe(false);
await delay();
// Make sure lemmy beta sees post is undeleted // Make sure lemmy beta sees post is undeleted
let searchBeta2 = await searchPost(beta, postRes.post_view.post); let searchBeta2 = await searchPost(beta, postRes.post);
let betaPost2 = searchBeta2.posts[0]; let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.post.deleted).toBe(false); expect(betaPost2.deleted).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post);
// Make sure lemmy beta cannot delete the post // Make sure lemmy beta cannot delete the post
let deletedPostBeta = await deletePost(beta, true, betaPost2.post); let deletedPostBeta = await deletePost(beta, true, betaPost2);
expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
}); });
test('Remove a post from admin and community on different instance', async () => { test('Remove a post from admin and community on different instance', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let removedPost = await removePost(alpha, true, postRes.post_view.post); let removedPost = await removePost(alpha, true, postRes.post);
expect(removedPost.post_view.post.removed).toBe(true); expect(removedPost.post.removed).toBe(true);
await delay();
// Make sure lemmy beta sees post is NOT removed // Make sure lemmy beta sees post is NOT removed
let searchBeta = await searchPost(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost.post.removed).toBe(false); expect(betaPost.removed).toBe(false);
await delay();
// Undelete // Undelete
let undeletedPost = await removePost(alpha, false, postRes.post_view.post); let undeletedPost = await removePost(alpha, false, postRes.post);
expect(undeletedPost.post_view.post.removed).toBe(false); expect(undeletedPost.post.removed).toBe(false);
await delay();
// Make sure lemmy beta sees post is undeleted // Make sure lemmy beta sees post is undeleted
let searchBeta2 = await searchPost(beta, postRes.post_view.post); let searchBeta2 = await searchPost(beta, postRes.post);
let betaPost2 = searchBeta2.posts[0]; let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.post.removed).toBe(false); expect(betaPost2.removed).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post);
}); });
test('Remove a post from admin and community on same instance', async () => { test('Remove a post from admin and community on same instance', async () => {
await followBeta(alpha); let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, search.communities[0].id);
expect(postRes.post_view.post).toBeDefined(); await longDelay();
// Get the id for beta // Get the id for beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post); let searchBeta = await searchPost(beta, postRes.post);
let betaPost = searchBeta.posts[0]; let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined(); await longDelay();
// The beta admin removes it (the community lives on beta) // The beta admin removes it (the community lives on beta)
let removePostRes = await removePost(beta, true, betaPost.post); let removePostRes = await removePost(beta, true, betaPost);
expect(removePostRes.post_view.post.removed).toBe(true); expect(removePostRes.post.removed).toBe(true);
await longDelay();
// Make sure lemmy alpha sees post is removed // Make sure lemmy alpha sees post is removed
let alphaPost = await getPost(alpha, postRes.post_view.post.id); let alphaPost = await getPost(alpha, postRes.post.id);
// expect(alphaPost.post_view.post.removed).toBe(true); // TODO this shouldn't be commented expect(alphaPost.post.removed).toBe(true);
// assertPostFederation(alphaPost.post_view, removePostRes.post_view); assertPostFederation(alphaPost.post, removePostRes.post);
await longDelay();
// Undelete // Undelete
let undeletedPost = await removePost(beta, false, betaPost.post); let undeletedPost = await removePost(beta, false, betaPost);
expect(undeletedPost.post_view.post.removed).toBe(false); expect(undeletedPost.post.removed).toBe(false);
await longDelay();
// Make sure lemmy alpha sees post is undeleted // Make sure lemmy alpha sees post is undeleted
let alphaPost2 = await getPost(alpha, postRes.post_view.post.id); let alphaPost2 = await getPost(alpha, postRes.post.id);
expect(alphaPost2.post_view.post.removed).toBe(false); await delay();
assertPostFederation(alphaPost2.post_view, undeletedPost.post_view); expect(alphaPost2.post.removed).toBe(false);
await unfollowRemotes(alpha); assertPostFederation(alphaPost2.post, undeletedPost.post);
}); });
test('Search for a post', async () => { test('Search for a post', async () => {
await unfollowRemotes(alpha); let search = await searchForBetaCommunity(alpha);
let postRes = await createPost(alpha, betaCommunity.community.id); await delay();
expect(postRes.post_view.post).toBeDefined(); let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let searchBeta = await searchPost(beta, postRes.post);
let searchBeta = await searchPost(beta, postRes.post_view.post); expect(searchBeta.posts[0].name).toBeDefined();
expect(searchBeta.posts[0].post.name).toBeDefined();
}); });
test('A and G subscribe to B (center) A posts, it gets announced to G', async () => { test('A and G subscribe to B (center) A posts, it gets announced to G', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let search = await searchForBetaCommunity(alpha);
expect(postRes.post_view.post).toBeDefined(); let postRes = await createPost(alpha, search.communities[0].id);
await delay();
let search2 = await searchPost(gamma, postRes.post_view.post); let search2 = await searchPost(gamma, postRes.post);
expect(search2.posts[0].post.name).toBeDefined(); expect(search2.posts[0].name).toBeDefined();
});
test('Enforce site ban for federated user', async () => {
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let userSearch = await searchForUser(beta, alphaShortname);
let alphaUser = userSearch.users[0];
expect(alphaUser).toBeDefined();
// ban alpha from beta site
let banAlpha = await banPersonFromSite(beta, alphaUser.person.id, true);
expect(banAlpha.banned).toBe(true);
// Alpha makes post on beta
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true);
expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post doesn't make it to beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banPersonFromSite(beta, alphaUser.person.id, false);
expect(unBanAlpha.banned).toBe(false);
});
test('Enforce community ban for federated user', async () => {
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let userSearch = await searchForUser(beta, alphaShortname);
let alphaUser = userSearch.users[0];
expect(alphaUser).toBeDefined();
// ban alpha from beta site
await banPersonFromCommunity(beta, alphaUser.person.id, 2, false);
let banAlpha = await banPersonFromCommunity(beta, alphaUser.person.id, 2, true);
expect(banAlpha.banned).toBe(true);
// Alpha makes post on beta
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true);
expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post doesn't make it to beta community
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banPersonFromCommunity(
beta,
alphaUser.person.id,
2,
false
);
expect(unBanAlpha.banned).toBe(false);
}); });

View file

@ -5,10 +5,12 @@ import {
setupLogins, setupLogins,
followBeta, followBeta,
createPrivateMessage, createPrivateMessage,
editPrivateMessage, updatePrivateMessage,
listPrivateMessages, listPrivateMessages,
deletePrivateMessage, deletePrivateMessage,
unfollowRemotes, unfollowRemotes,
delay,
longDelay,
} from './shared'; } from './shared';
let recipient_id: number; let recipient_id: number;
@ -16,7 +18,8 @@ let recipient_id: number;
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
let follow = await followBeta(alpha); let follow = await followBeta(alpha);
recipient_id = follow.community_view.creator.id; await longDelay();
recipient_id = follow.community.creator_id;
}); });
afterAll(async () => { afterAll(async () => {
@ -25,66 +28,55 @@ afterAll(async () => {
test('Create a private message', async () => { test('Create a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id); let pmRes = await createPrivateMessage(alpha, recipient_id);
expect(pmRes.private_message_view.private_message.content).toBeDefined(); expect(pmRes.message.content).toBeDefined();
expect(pmRes.private_message_view.private_message.local).toBe(true); expect(pmRes.message.local).toBe(true);
expect(pmRes.private_message_view.creator.local).toBe(true); expect(pmRes.message.creator_local).toBe(true);
expect(pmRes.private_message_view.recipient.local).toBe(false); expect(pmRes.message.recipient_local).toBe(false);
await delay();
let betaPms = await listPrivateMessages(beta); let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBeDefined(); expect(betaPms.messages[0].content).toBeDefined();
expect(betaPms.private_messages[0].private_message.local).toBe(false); expect(betaPms.messages[0].local).toBe(false);
expect(betaPms.private_messages[0].creator.local).toBe(false); expect(betaPms.messages[0].creator_local).toBe(false);
expect(betaPms.private_messages[0].recipient.local).toBe(true); expect(betaPms.messages[0].recipient_local).toBe(true);
}); });
test('Update a private message', async () => { test('Update a private message', async () => {
let updatedContent = 'A jest test federated private message edited'; 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( let pmUpdated = await updatePrivateMessage(alpha, pmRes.message.id);
alpha, expect(pmUpdated.message.content).toBe(updatedContent);
pmRes.private_message_view.private_message.id await longDelay();
);
expect(pmUpdated.private_message_view.private_message.content).toBe(
updatedContent
);
let betaPms = await listPrivateMessages(beta); let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBe( expect(betaPms.messages[0].content).toBe(updatedContent);
updatedContent
);
}); });
test('Delete a private message', async () => { test('Delete a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id); let pmRes = await createPrivateMessage(alpha, recipient_id);
await delay();
let betaPms1 = await listPrivateMessages(beta); let betaPms1 = await listPrivateMessages(beta);
let deletedPmRes = await deletePrivateMessage( let deletedPmRes = await deletePrivateMessage(alpha, true, pmRes.message.id);
alpha, expect(deletedPmRes.message.deleted).toBe(true);
true, await delay();
pmRes.private_message_view.private_message.id
);
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
// 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 listPrivateMessages(beta); let betaPms2 = await listPrivateMessages(beta);
expect(betaPms2.private_messages.length).toBe( expect(betaPms2.messages.length).toBe(betaPms1.messages.length - 1);
betaPms1.private_messages.length - 1 await delay();
);
// 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 listPrivateMessages(beta); let betaPms3 = await listPrivateMessages(beta);
expect(betaPms3.private_messages.length).toBe( expect(betaPms3.messages.length).toBe(betaPms1.messages.length);
betaPms1.private_messages.length
);
}); });

View file

@ -1,54 +1,48 @@
import { import {
Login, LoginForm,
LoginResponse, LoginResponse,
CreatePost, Post,
EditPost, PostForm,
CreateComment, Comment,
DeletePost, DeletePostForm,
RemovePost, RemovePostForm,
StickyPost, StickyPostForm,
LockPost, LockPostForm,
PostResponse, PostResponse,
SearchResponse, SearchResponse,
FollowCommunity, FollowCommunityForm,
CommunityResponse, CommunityResponse,
GetFollowedCommunitiesResponse, GetFollowedCommunitiesResponse,
GetPostResponse, GetPostResponse,
Register, RegisterForm,
Comment, CommentForm,
EditComment, DeleteCommentForm,
DeleteComment, RemoveCommentForm,
RemoveComment, SearchForm,
Search,
CommentResponse, CommentResponse,
GetCommunity, GetCommunityForm,
CreateCommunity, CommunityForm,
DeleteCommunity, DeleteCommunityForm,
RemoveCommunity, RemoveCommunityForm,
GetPersonMentions, GetUserMentionsForm,
CreateCommentLike, CommentLikeForm,
CreatePostLike, CreatePostLikeForm,
EditPrivateMessage, PrivateMessageForm,
DeletePrivateMessage, EditPrivateMessageForm,
GetFollowedCommunities, DeletePrivateMessageForm,
GetPrivateMessages, GetFollowedCommunitiesForm,
GetSite, GetPrivateMessagesForm,
GetPost, GetSiteForm,
GetPostForm,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
GetPersonMentionsResponse, GetUserMentionsResponse,
SaveUserSettings, UserSettingsForm,
SortType, SortType,
ListingType, ListingType,
GetSiteResponse, GetSiteResponse,
SearchType, SearchType,
LemmyHttp, LemmyHttp,
BanPersonResponse,
BanPerson,
BanFromCommunity,
BanFromCommunityResponse,
Post,
CreatePrivateMessage,
} from 'lemmy-js-client'; } from 'lemmy-js-client';
export interface API { export interface API {
@ -57,27 +51,27 @@ export interface API {
} }
export let alpha: API = { export let alpha: API = {
client: new LemmyHttp('http://localhost:8541/api/v2'), client: new LemmyHttp('http://localhost:8541/api/v1'),
}; };
export let beta: API = { export let beta: API = {
client: new LemmyHttp('http://localhost:8551/api/v2'), client: new LemmyHttp('http://localhost:8551/api/v1'),
}; };
export let gamma: API = { export let gamma: API = {
client: new LemmyHttp('http://localhost:8561/api/v2'), client: new LemmyHttp('http://localhost:8561/api/v1'),
}; };
export let delta: API = { export let delta: API = {
client: new LemmyHttp('http://localhost:8571/api/v2'), client: new LemmyHttp('http://localhost:8571/api/v1'),
}; };
export let epsilon: API = { export let epsilon: API = {
client: new LemmyHttp('http://localhost:8581/api/v2'), client: new LemmyHttp('http://localhost:8581/api/v1'),
}; };
export async function setupLogins() { export async function setupLogins() {
let formAlpha: Login = { let formAlpha: LoginForm = {
username_or_email: 'lemmy_alpha', username_or_email: 'lemmy_alpha',
password: 'lemmy', password: 'lemmy',
}; };
@ -126,10 +120,10 @@ export async function createPost(
api: API, api: API,
community_id: number community_id: number
): Promise<PostResponse> { ): Promise<PostResponse> {
let name = randomString(5); let name = 'A jest test post';
let body = randomString(10); let body = 'Some body';
let url = 'https://google.com/'; let url = 'https://google.com/';
let form: CreatePost = { let form: PostForm = {
name, name,
url, url,
body, body,
@ -140,11 +134,11 @@ export async function createPost(
return api.client.createPost(form); return api.client.createPost(form);
} }
export async function editPost(api: API, post: Post): Promise<PostResponse> { export async function updatePost(api: API, post: Post): Promise<PostResponse> {
let name = 'A jest test federated post, updated'; let name = 'A jest test federated post, updated';
let form: EditPost = { let form: PostForm = {
name, name,
post_id: post.id, edit_id: post.id,
auth: api.auth, auth: api.auth,
nsfw: false, nsfw: false,
}; };
@ -156,8 +150,8 @@ export async function deletePost(
deleted: boolean, deleted: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: DeletePost = { let form: DeletePostForm = {
post_id: post.id, edit_id: post.id,
deleted: deleted, deleted: deleted,
auth: api.auth, auth: api.auth,
}; };
@ -169,8 +163,8 @@ export async function removePost(
removed: boolean, removed: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: RemovePost = { let form: RemovePostForm = {
post_id: post.id, edit_id: post.id,
removed, removed,
auth: api.auth, auth: api.auth,
}; };
@ -182,8 +176,8 @@ export async function stickyPost(
stickied: boolean, stickied: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: StickyPost = { let form: StickyPostForm = {
post_id: post.id, edit_id: post.id,
stickied, stickied,
auth: api.auth, auth: api.auth,
}; };
@ -195,8 +189,8 @@ export async function lockPost(
locked: boolean, locked: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: LockPost = { let form: LockPostForm = {
post_id: post.id, edit_id: post.id,
locked, locked,
auth: api.auth, auth: api.auth,
}; };
@ -207,7 +201,7 @@ export async function searchPost(
api: API, api: API,
post: Post post: Post
): Promise<SearchResponse> { ): Promise<SearchResponse> {
let form: Search = { let form: SearchForm = {
q: post.ap_id, q: post.ap_id,
type_: SearchType.Posts, type_: SearchType.Posts,
sort: SortType.TopAll, sort: SortType.TopAll,
@ -215,23 +209,11 @@ export async function searchPost(
return api.client.search(form); return api.client.search(form);
} }
export async function searchPostLocal(
api: API,
post: Post
): Promise<SearchResponse> {
let form: Search = {
q: post.name,
type_: SearchType.Posts,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function getPost( export async function getPost(
api: API, api: API,
post_id: number post_id: number
): Promise<GetPostResponse> { ): Promise<GetPostResponse> {
let form: GetPost = { let form: GetPostForm = {
id: post_id, id: post_id,
}; };
return api.client.getPost(form); return api.client.getPost(form);
@ -241,7 +223,7 @@ export async function searchComment(
api: API, api: API,
comment: Comment comment: Comment
): Promise<SearchResponse> { ): Promise<SearchResponse> {
let form: Search = { let form: SearchForm = {
q: comment.ap_id, q: comment.ap_id,
type_: SearchType.Comments, type_: SearchType.Comments,
sort: SortType.TopAll, sort: SortType.TopAll,
@ -254,7 +236,7 @@ export async function searchForBetaCommunity(
): Promise<SearchResponse> { ): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url // Use short-hand search url
let form: Search = { let form: SearchForm = {
q: '!main@lemmy-beta:8551', q: '!main@lemmy-beta:8551',
type_: SearchType.Communities, type_: SearchType.Communities,
sort: SortType.TopAll, sort: SortType.TopAll,
@ -264,10 +246,10 @@ export async function searchForBetaCommunity(
export async function searchForCommunity( export async function searchForCommunity(
api: API, api: API,
q: string q: string,
): Promise<SearchResponse> { ): Promise<SearchResponse> {
// Use short-hand search url // Use short-hand search url
let form: Search = { let form: SearchForm = {
q, q,
type_: SearchType.Communities, type_: SearchType.Communities,
sort: SortType.TopAll, sort: SortType.TopAll,
@ -281,7 +263,7 @@ export async function searchForUser(
): Promise<SearchResponse> { ): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url // Use short-hand search url
let form: Search = { let form: SearchForm = {
q: apShortname, q: apShortname,
type_: SearchType.Users, type_: SearchType.Users,
sort: SortType.TopAll, sort: SortType.TopAll,
@ -289,46 +271,12 @@ export async function searchForUser(
return api.client.search(form); return api.client.search(form);
} }
export async function banPersonFromSite(
api: API,
person_id: number,
ban: boolean
): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: BanPerson = {
person_id,
ban,
remove_data: false,
auth: api.auth,
};
return api.client.banPerson(form);
}
export async function banPersonFromCommunity(
api: API,
person_id: number,
community_id: number,
ban: boolean
): Promise<BanFromCommunityResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: BanFromCommunity = {
person_id,
community_id,
remove_data: false,
ban,
auth: api.auth,
};
return api.client.banFromCommunity(form);
}
export async function followCommunity( export async function followCommunity(
api: API, api: API,
follow: boolean, follow: boolean,
community_id: number community_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: FollowCommunity = { let form: FollowCommunityForm = {
community_id, community_id,
follow, follow,
auth: api.auth, auth: api.auth,
@ -339,7 +287,7 @@ export async function followCommunity(
export async function checkFollowedCommunities( export async function checkFollowedCommunities(
api: API api: API
): Promise<GetFollowedCommunitiesResponse> { ): Promise<GetFollowedCommunitiesResponse> {
let form: GetFollowedCommunities = { let form: GetFollowedCommunitiesForm = {
auth: api.auth, auth: api.auth,
}; };
return api.client.getFollowedCommunities(form); return api.client.getFollowedCommunities(form);
@ -350,7 +298,7 @@ export async function likePost(
score: number, score: number,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: CreatePostLike = { let form: CreatePostLikeForm = {
post_id: post.id, post_id: post.id,
score: score, score: score,
auth: api.auth, auth: api.auth,
@ -365,7 +313,7 @@ export async function createComment(
parent_id?: number, parent_id?: number,
content = 'a jest test comment' content = 'a jest test comment'
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: CreateComment = { let form: CommentForm = {
content, content,
post_id, post_id,
parent_id, parent_id,
@ -374,14 +322,14 @@ export async function createComment(
return api.client.createComment(form); return api.client.createComment(form);
} }
export async function editComment( export async function updateComment(
api: API, api: API,
comment_id: number, edit_id: number,
content = 'A jest test federated comment update' content = 'A jest test federated comment update'
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: EditComment = { let form: CommentForm = {
content, content,
comment_id, edit_id,
auth: api.auth, auth: api.auth,
}; };
return api.client.editComment(form); return api.client.editComment(form);
@ -390,10 +338,10 @@ export async function editComment(
export async function deleteComment( export async function deleteComment(
api: API, api: API,
deleted: boolean, deleted: boolean,
comment_id: number edit_id: number
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: DeleteComment = { let form: DeleteCommentForm = {
comment_id, edit_id,
deleted, deleted,
auth: api.auth, auth: api.auth,
}; };
@ -403,23 +351,23 @@ export async function deleteComment(
export async function removeComment( export async function removeComment(
api: API, api: API,
removed: boolean, removed: boolean,
comment_id: number edit_id: number
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: RemoveComment = { let form: RemoveCommentForm = {
comment_id, edit_id,
removed, removed,
auth: api.auth, auth: api.auth,
}; };
return api.client.removeComment(form); return api.client.removeComment(form);
} }
export async function getMentions(api: API): Promise<GetPersonMentionsResponse> { export async function getMentions(api: API): Promise<GetUserMentionsResponse> {
let form: GetPersonMentions = { let form: GetUserMentionsForm = {
sort: SortType.New, sort: SortType.New,
unread_only: false, unread_only: false,
auth: api.auth, auth: api.auth,
}; };
return api.client.getPersonMentions(form); return api.client.getUserMentions(form);
} }
export async function likeComment( export async function likeComment(
@ -427,7 +375,7 @@ export async function likeComment(
score: number, score: number,
comment: Comment comment: Comment
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: CreateCommentLike = { let form: CommentLikeForm = {
comment_id: comment.id, comment_id: comment.id,
score, score,
auth: api.auth, auth: api.auth,
@ -442,12 +390,13 @@ export async function createCommunity(
let description = 'a sample description'; let description = 'a sample description';
let icon = 'https://image.flaticon.com/icons/png/512/35/35896.png'; let icon = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let banner = 'https://image.flaticon.com/icons/png/512/35/35896.png'; let banner = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let form: CreateCommunity = { let form: CommunityForm = {
name: name_, name: name_,
title: name_, title: name_,
description, description,
icon, icon,
banner, banner,
category_id: 1,
nsfw: false, nsfw: false,
auth: api.auth, auth: api.auth,
}; };
@ -456,9 +405,9 @@ export async function createCommunity(
export async function getCommunity( export async function getCommunity(
api: API, api: API,
id: number id: number,
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: GetCommunity = { let form: GetCommunityForm = {
id, id,
}; };
return api.client.getCommunity(form); return api.client.getCommunity(form);
@ -467,10 +416,10 @@ export async function getCommunity(
export async function deleteCommunity( export async function deleteCommunity(
api: API, api: API,
deleted: boolean, deleted: boolean,
community_id: number edit_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: DeleteCommunity = { let form: DeleteCommunityForm = {
community_id, edit_id,
deleted, deleted,
auth: api.auth, auth: api.auth,
}; };
@ -480,10 +429,10 @@ export async function deleteCommunity(
export async function removeCommunity( export async function removeCommunity(
api: API, api: API,
removed: boolean, removed: boolean,
community_id: number edit_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: RemoveCommunity = { let form: RemoveCommunityForm = {
community_id, edit_id,
removed, removed,
auth: api.auth, auth: api.auth,
}; };
@ -495,7 +444,7 @@ export async function createPrivateMessage(
recipient_id: number recipient_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let content = 'A jest test federated private message'; let content = 'A jest test federated private message';
let form: CreatePrivateMessage = { let form: PrivateMessageForm = {
content, content,
recipient_id, recipient_id,
auth: api.auth, auth: api.auth,
@ -503,14 +452,14 @@ export async function createPrivateMessage(
return api.client.createPrivateMessage(form); return api.client.createPrivateMessage(form);
} }
export async function editPrivateMessage( export async function updatePrivateMessage(
api: API, api: API,
private_message_id: number edit_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let updatedContent = 'A jest test federated private message edited'; let updatedContent = 'A jest test federated private message edited';
let form: EditPrivateMessage = { let form: EditPrivateMessageForm = {
content: updatedContent, content: updatedContent,
private_message_id, edit_id,
auth: api.auth, auth: api.auth,
}; };
return api.client.editPrivateMessage(form); return api.client.editPrivateMessage(form);
@ -519,11 +468,11 @@ export async function editPrivateMessage(
export async function deletePrivateMessage( export async function deletePrivateMessage(
api: API, api: API,
deleted: boolean, deleted: boolean,
private_message_id: number edit_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let form: DeletePrivateMessage = { let form: DeletePrivateMessageForm = {
deleted, deleted,
private_message_id, edit_id,
auth: api.auth, auth: api.auth,
}; };
return api.client.deletePrivateMessage(form); return api.client.deletePrivateMessage(form);
@ -533,10 +482,11 @@ export async function registerUser(
api: API, api: API,
username: string = randomString(5) username: string = randomString(5)
): Promise<LoginResponse> { ): Promise<LoginResponse> {
let form: Register = { let form: RegisterForm = {
username, username,
password: 'test', password: 'test',
password_verify: 'test', password_verify: 'test',
admin: false,
show_nsfw: true, show_nsfw: true,
}; };
return api.client.register(form); return api.client.register(form);
@ -546,7 +496,7 @@ export async function saveUserSettingsBio(
api: API, api: API,
auth: string auth: string
): Promise<LoginResponse> { ): Promise<LoginResponse> {
let form: SaveUserSettings = { let form: UserSettingsForm = {
show_nsfw: true, show_nsfw: true,
theme: 'darkly', theme: 'darkly',
default_sort_type: Object.keys(SortType).indexOf(SortType.Active), default_sort_type: Object.keys(SortType).indexOf(SortType.Active),
@ -562,7 +512,7 @@ export async function saveUserSettingsBio(
export async function saveUserSettings( export async function saveUserSettings(
api: API, api: API,
form: SaveUserSettings form: UserSettingsForm
): Promise<LoginResponse> { ): Promise<LoginResponse> {
return api.client.saveUserSettings(form); return api.client.saveUserSettings(form);
} }
@ -571,7 +521,7 @@ export async function getSite(
api: API, api: API,
auth: string auth: string
): Promise<GetSiteResponse> { ): Promise<GetSiteResponse> {
let form: GetSite = { let form: GetSiteForm = {
auth, auth,
}; };
return api.client.getSite(form); return api.client.getSite(form);
@ -580,7 +530,7 @@ export async function getSite(
export async function listPrivateMessages( export async function listPrivateMessages(
api: API api: API
): Promise<PrivateMessagesResponse> { ): Promise<PrivateMessagesResponse> {
let form: GetPrivateMessages = { let form: GetPrivateMessagesForm = {
auth: api.auth, auth: api.auth,
unread_only: false, unread_only: false,
limit: 999, limit: 999,
@ -594,27 +544,31 @@ export async function unfollowRemotes(
// Unfollow all remote communities // Unfollow all remote communities
let followed = await checkFollowedCommunities(api); let followed = await checkFollowedCommunities(api);
let remoteFollowed = followed.communities.filter( let remoteFollowed = followed.communities.filter(
c => c.community.local == false c => c.community_local == false
); );
for (let cu of remoteFollowed) { for (let cu of remoteFollowed) {
await followCommunity(api, false, cu.community.id); await followCommunity(api, false, cu.community_id);
} }
let followed2 = await checkFollowedCommunities(api); let followed2 = await checkFollowedCommunities(api);
return followed2; return followed2;
} }
export async function followBeta(api: API): Promise<CommunityResponse> { export async function followBeta(api: API): Promise<CommunityResponse> {
await unfollowRemotes(api);
// Cache it // Cache it
let search = await searchForBetaCommunity(api); let search = await searchForBetaCommunity(api);
let com = search.communities.find(c => c.community.local == false); let com = search.communities.filter(c => c.local == false);
if (com) { if (com[0]) {
let follow = await followCommunity(api, true, com.community.id); let follow = await followCommunity(api, true, com[0].id);
return follow; return follow;
} }
} }
export function delay(millis: number = 500) { export function delay(millis: number = 500) {
return new Promise(resolve => setTimeout(resolve, millis)); return new Promise((resolve, _reject) => {
setTimeout(_ => resolve(), millis);
});
} }
export function longDelay() { export function longDelay() {

View file

@ -4,27 +4,28 @@ import {
beta, beta,
registerUser, registerUser,
searchForUser, searchForUser,
saveUserSettingsBio,
saveUserSettings, saveUserSettings,
getSite, getSite,
} from './shared'; } from './shared';
import { import {
PersonViewSafe, UserView,
SaveUserSettings, UserSettingsForm,
SortType,
ListingType,
} from 'lemmy-js-client'; } from 'lemmy-js-client';
let auth: string; let auth: string;
let apShortname: string; let apShortname: string;
function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) { function assertUserFederation(
expect(userOne.person.name).toBe(userTwo.person.name); userOne: UserView,
expect(userOne.person.preferred_username).toBe(userTwo.person.preferred_username); 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 () => {
@ -34,30 +35,42 @@ test('Create user', async () => {
let site = await getSite(alpha, auth); let site = await getSite(alpha, auth);
expect(site.my_user).toBeDefined(); expect(site.my_user).toBeDefined();
apShortname = `@${site.my_user.person.name}@lemmy-alpha:8541`; apShortname = `@${site.my_user.name}@lemmy-alpha:8541`;
}); });
test('Set some user settings, check that they are federated', async () => { 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 avatar = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png'; let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png';
let bio = 'a changed bio'; let form: UserSettingsForm = {
let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
theme: '', theme: "",
default_sort_type: Object.keys(SortType).indexOf(SortType.Hot), default_sort_type: 0,
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All), default_listing_type: 0,
lang: '', lang: "",
avatar, avatar,
banner, banner,
preferred_username: 'user321', preferred_username: "user321",
show_avatars: false, show_avatars: false,
send_notifications_to_email: false, send_notifications_to_email: false,
bio,
auth, auth,
}; }
await saveUserSettings(alpha, form); let settingsRes = await saveUserSettings(alpha, form);
let searchAlpha = await searchForUser(alpha, apShortname); let searchAlpha = await searchForUser(beta, apShortname);
let userOnAlpha = searchAlpha.users[0]; let userOnAlpha = searchAlpha.users[0];
let searchBeta = await searchForUser(beta, apShortname); let searchBeta = await searchForUser(beta, apShortname);
let userOnBeta = searchBeta.users[0]; let userOnBeta = searchBeta.users[0];

View file

@ -1,16 +0,0 @@
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist",
"module": "CommonJS",
"noImplicitAny": true,
"lib": ["es2017", "es7", "es6", "dom"],
"outDir": "./dist",
"target": "ES5",
"moduleResolution": "Node"
},
"include": [
"src/**/*"
],
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load diff

7
clean.sh Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
cargo update
cargo fmt
cargo check
cargo clippy
cargo outdated -R

View file

@ -25,7 +25,7 @@
# maximum number of active sql connections # maximum number of active sql connections
pool_size: 5 pool_size: 5
} }
# the domain name of your instance (eg "lemmy.ml") # the domain name of your instance (eg "dev.lemmy.ml")
hostname: null hostname: null
# address where lemmy should listen for incoming requests # address where lemmy should listen for incoming requests
bind: "0.0.0.0" bind: "0.0.0.0"
@ -35,6 +35,8 @@
tls_enabled: true tls_enabled: true
# json web token for authorization between server and client # json web token for authorization between server and client
jwt_secret: "changeme" jwt_secret: "changeme"
# path to built documentation
docs_dir: "/app/documentation"
# address where pictrs is available # address where pictrs is available
pictrs_url: "http://pictrs:8080" pictrs_url: "http://pictrs:8080"
# address where iframely is available # address where iframely is available
@ -63,13 +65,12 @@
# whether to enable activitypub federation. # whether to enable activitypub federation.
enabled: false enabled: false
# Allows and blocks are described here: # Allows and blocks are described here:
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist # https://dev.lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
# #
# comma separated list of instances with which federation is allowed # comma separated list of instances with which federation is allowed
# Only one of these blocks should be uncommented allowed_instances: ""
# allowed_instances: ["instance1.tld","instance2.tld"]
# comma separated list of instances which are blocked from federating # comma separated list of instances which are blocked from federating
# blocked_instances: [] blocked_instances: ""
} }
captcha: { captcha: {
enabled: true enabled: true

View file

@ -1,50 +0,0 @@
[package]
name = "lemmy_api"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_api"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_apub = { path = "../apub" }
lemmy_utils = { path = "../utils" }
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_moderator = { path = "../db_views_moderator" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_api_structs = { path = "../api_structs" }
lemmy_websocket = { path = "../websocket" }
diesel = "1.4.5"
bcrypt = "0.9.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false }
actix-rt = { version = "1.1.1", default-features = false }
awc = { version = "2.0.3", default-features = false }
log = "0.4.14"
rand = "0.8.3"
strum = "0.20.0"
strum_macros = "0.20.1"
lazy_static = "1.4.0"
url = { version = "2.2.1", features = ["serde"] }
openssl = "0.10.32"
http = "0.2.3"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "0.3.6"
futures = "0.3.12"
itertools = "0.10.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
sha2 = "0.9.3"
async-trait = "0.1.42"
captcha = "0.0.8"
anyhow = "1.0.38"
thiserror = "1.0.23"
background-jobs = "0.8.0"
reqwest = { version = "0.10.10", features = ["json"] }

View file

@ -1,876 +0,0 @@
use crate::{
check_community_ban,
check_downvotes_enabled,
collect_moderated_communities,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
get_post,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use lemmy_api_structs::{blocking, comment::*, send_local_notifs};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_db_queries::{
source::comment::Comment_,
Crud,
Likeable,
ListingType,
Reportable,
Saveable,
SortType,
};
use lemmy_db_schema::{
source::{comment::*, comment_report::*, moderator::*},
LocalUserId,
};
use lemmy_db_views::{
comment_report_view::{CommentReportQueryBuilder, CommentReportView},
comment_view::{CommentQueryBuilder, CommentView},
local_user_view::LocalUserView,
};
use lemmy_utils::{
utils::{remove_slurs, scrape_text_for_mentions},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for CreateComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let content_slurs_removed = remove_slurs(&data.content.to_owned());
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, context.pool()).await?;
check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
// Check if post is locked, no new comments
if post.locked {
return Err(ApiError::err("locked").into());
}
// If there's a parent_id, check to make sure that comment is in that post
if let Some(parent_id) = data.parent_id {
// Make sure the parent comment exists
let parent =
match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
if parent.post_id != post_id {
return Err(ApiError::err("couldnt_create_comment").into());
}
}
let comment_form = CommentForm {
content: content_slurs_removed,
parent_id: data.parent_id.to_owned(),
post_id: data.post_id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
// Create the comment
let comment_form2 = comment_form.clone();
let inserted_comment = match blocking(context.pool(), move |conn| {
Comment::create(&conn, &comment_form2)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment =
match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
let apub_id =
generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
updated_comment
.send_create(&local_user_view.person, context)
.await?;
// Scan the comment for user mentions, add those rows
let post_id = post.id;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(
mentions,
updated_comment.clone(),
local_user_view.person.clone(),
post,
context.pool(),
true,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id,
person_id: local_user_view.person.id,
score: 1,
};
let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(ApiError::err("couldnt_like_comment").into());
}
updated_comment
.send_like(&local_user_view.person, context)
.await?;
let person_id = local_user_view.person.id;
let mut comment_view = blocking(context.pool(), move |conn| {
CommentView::read(&conn, inserted_comment.id, Some(person_id))
})
.await??;
// If its a comment to yourself, mark it as read
let comment_id = comment_view.comment.id;
if local_user_view.person.id == comment_view.get_recipient_id() {
match blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, true)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
comment_view.comment.read = true;
}
let mut res = CommentResponse {
comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateComment,
comment: res.clone(),
websocket_id,
});
res.recipient_ids = Vec::new(); // Necessary to avoid doubles
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can edit
if local_user_view.person.id != orig_comment.creator.id {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the update
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let comment_id = data.comment_id;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_content(conn, comment_id, &content_slurs_removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub update
updated_comment
.send_update(&local_user_view.person, context)
.await?;
// Do the mentions / recipients
let updated_comment_content = updated_comment.content.to_owned();
let mentions = scrape_text_for_mentions(&updated_comment_content);
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
orig_comment.post,
context.pool(),
false,
)
.await?;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeleteComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can delete
if local_user_view.person.id != orig_comment.creator.id {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the delete
let deleted = data.deleted;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment_id, deleted)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub message
if deleted {
updated_comment
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_comment
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// Build the recipients
let comment_view_2 = comment_view.clone();
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
)
.await?;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None, // TODO a comment delete might clear forms?
};
context.chat_server().do_send(SendComment {
op: UserOperation::DeleteComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemoveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only a mod or admin can remove
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_comment.community.id,
)
.await?;
// Do the remove
let removed = data.removed;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment_id, removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Mod tables
let form = ModRemoveCommentForm {
mod_person_id: local_user_view.person.id,
comment_id: data.comment_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(context.pool(), move |conn| {
ModRemoveComment::create(conn, &form)
})
.await??;
// Send the apub message
if removed {
updated_comment
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_comment
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// Build the recipients
let comment_view_2 = comment_view.clone();
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
)
.await?;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None, // TODO maybe this might clear other forms
};
context.chat_server().do_send(SendComment {
op: UserOperation::RemoveComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentAsRead {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the recipient can mark as read
if local_user_view.person.id != orig_comment.get_recipient_id() {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the mark as read
let read = data.read;
match blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, read)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
};
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SaveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
person_id: local_user_view.person.id,
};
if data.save {
let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
if blocking(context.pool(), save_comment).await?.is_err() {
return Err(ApiError::err("couldnt_save_comment").into());
}
} else {
let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
if blocking(context.pool(), unsave_comment).await?.is_err() {
return Err(ApiError::err("couldnt_save_comment").into());
}
}
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
Ok(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommentLike {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, 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, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Add parent user to recipients
let recipient_id = orig_comment.get_recipient_id();
if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, 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;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Only add the like if the score isnt 0
let comment = orig_comment.comment;
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(ApiError::err("couldnt_like_comment").into());
}
if like_form.score == 1 {
comment.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
comment
.send_dislike(&local_user_view.person, context)
.await?;
}
} else {
comment
.send_undo_like(&local_user_view.person, context)
.await?;
}
// Have to refetch the comment to get the current state
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let liked_comment = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view: liked_comment,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetComments {
type Response = GetCommentsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.listing_type(type_)
.sort(&sort)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await?;
let comments = match comments {
Ok(comments) => comments,
Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
};
Ok(GetCommentsResponse { comments })
}
}
/// Creates a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommentReport {
type Response = CreateCommentReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CreateCommentReportResponse, LemmyError> {
let data: &CreateCommentReport = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(ApiError::err("report_reason_required").into());
}
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let person_id = local_user_view.person.id;
let comment_id = data.comment_id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
let report_form = CommentReportForm {
creator_id: person_id,
comment_id,
original_comment_text: comment_view.comment.content,
reason: data.reason.to_owned(),
};
let report = match blocking(context.pool(), move |conn| {
CommentReport::report(conn, &report_form)
})
.await?
{
Ok(report) => report,
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreateCommentReportResponse { success: true };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreateCommentReport,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::CreateCommentReport,
response: report,
community_id: comment_view.community.id,
websocket_id,
});
Ok(res)
}
}
/// Resolves or unresolves a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for ResolveCommentReport {
type Response = ResolveCommentReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ResolveCommentReportResponse, LemmyError> {
let data: &ResolveCommentReport = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let report_id = data.report_id;
let report = blocking(context.pool(), move |conn| {
CommentReportView::read(&conn, report_id)
})
.await??;
let person_id = local_user_view.person.id;
is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
let resolved = data.resolved;
let resolve_fun = move |conn: &'_ _| {
if resolved {
CommentReport::resolve(conn, report_id, person_id)
} else {
CommentReport::unresolve(conn, report_id, person_id)
}
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(ApiError::err("couldnt_resolve_report").into());
};
let report_id = data.report_id;
let res = ResolveCommentReportResponse {
report_id,
resolved,
};
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::ResolveCommentReport,
response: res.clone(),
community_id: report.community.id,
websocket_id,
});
Ok(res)
}
}
/// Lists comment reports for a community if an id is supplied
/// or returns all comment reports for communities a user moderates
#[async_trait::async_trait(?Send)]
impl Perform for ListCommentReports {
type Response = ListCommentReportsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ListCommentReportsResponse, LemmyError> {
let data: &ListCommentReports = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
CommentReportQueryBuilder::create(conn)
.community_ids(community_ids)
.page(page)
.limit(limit)
.list()
})
.await??;
let res = ListCommentReportsResponse { comments };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListCommentReports,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
Ok(res)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,954 +0,0 @@
use crate::{
check_community_ban,
check_downvotes_enabled,
collect_moderated_communities,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use lemmy_api_structs::{blocking, post::*};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_db_queries::{
source::post::Post_,
Crud,
Likeable,
ListingType,
Reportable,
Saveable,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{
moderator::*,
post::*,
post_report::{PostReport, PostReportForm},
},
};
use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_report_view::{PostReportQueryBuilder, PostReportView},
post_view::{PostQueryBuilder, PostView},
};
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
};
use lemmy_utils::{
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, check_slurs_opt, is_valid_post_title},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for CreatePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(ApiError::err("invalid_post_title").into());
}
check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
// Fetch Iframely and pictrs cached image
let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await;
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data_url.map(|u| u.to_owned().into()),
body: data.body.to_owned(),
community_id: data.community_id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
locked: None,
stickied: None,
updated: None,
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: None,
local: true,
published: None,
};
let inserted_post =
match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
Ok(post) => post,
Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_create_post"
};
return Err(ApiError::err(err_type).into());
}
};
let inserted_post_id = inserted_post.id;
let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
};
updated_post
.send_create(&local_user_view.person, context)
.await?;
// They like their own post by default
let like_form = PostLikeForm {
post_id: inserted_post.id,
person_id: local_user_view.person.id,
score: 1,
};
let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(ApiError::err("couldnt_like_post").into());
}
updated_post
.send_like(&local_user_view.person, context)
.await?;
// Refetch the view
let inserted_post_id = inserted_post.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, inserted_post_id, Some(local_user_view.person.id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetPost {
type Response = GetPostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = &self;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let id = data.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, id, person_id)
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let id = data.id;
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.my_person_id(person_id)
.post_id(id)
.limit(9999)
.list()
})
.await??;
let community_id = post_view.community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
// Necessary for the sidebar
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, person_id)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
.chat_server()
.send(GetPostUsersOnline { post_id: data.id })
.await
.unwrap_or(1);
// Return the jwt
Ok(GetPostResponse {
post_view,
community_view,
comments,
moderators,
online,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetPosts {
type Response = GetPostsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = &self;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = match &local_user_view {
Some(uv) => Some(uv.person.id),
None => None,
};
let show_nsfw = match &local_user_view {
Some(uv) => uv.local_user.show_nsfw,
None => false,
};
let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?;
let page = data.page;
let limit = data.limit;
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
let posts = match blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.listing_type(&type_)
.sort(&sort)
.show_nsfw(show_nsfw)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await?
{
Ok(posts) => posts,
Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
};
Ok(GetPostsResponse { posts })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostLike {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePostLike = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, context.pool()).await?;
// Check for a community ban
let post_id = data.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(local_user_view.person.id, post.community_id, 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;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, 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 {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(ApiError::err("couldnt_like_post").into());
}
if like_form.score == 1 {
post.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
post.send_dislike(&local_user_view.person, context).await?;
}
} else {
post
.send_undo_like(&local_user_view.person, context)
.await?;
}
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(person_id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &EditPost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(ApiError::err("invalid_post_title").into());
}
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the creator can edit
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Fetch Iframely and Pictrs cached image
let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data_url).await;
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data_url.map(|u| u.to_owned().into()),
body: data.body.to_owned(),
nsfw: data.nsfw,
creator_id: orig_post.creator_id.to_owned(),
community_id: orig_post.community_id,
removed: Some(orig_post.removed),
deleted: Some(orig_post.deleted),
locked: Some(orig_post.locked),
stickied: Some(orig_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(orig_post.ap_id),
local: orig_post.local,
published: None,
};
let post_id = data.post_id;
let res = blocking(context.pool(), move |conn| {
Post::update(conn, post_id, &post_form)
})
.await?;
let updated_post: Post = match res {
Ok(post) => post,
Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_update_post"
};
return Err(ApiError::err(err_type).into());
}
};
// Send apub update
updated_post
.send_update(&local_user_view.person, context)
.await?;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeletePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &DeletePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the creator can delete
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Update the post
let post_id = data.post_id;
let deleted = data.deleted;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post_id, deleted)
})
.await??;
// apub updates
if deleted {
updated_post
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_post
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::DeletePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemovePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &RemovePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can remove
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let removed = data.removed;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post_id, removed)
})
.await??;
// Mod tables
let form = ModRemovePostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(context.pool(), move |conn| {
ModRemovePost::create(conn, &form)
})
.await??;
// apub updates
if removed {
updated_post
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_post
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch the post
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(person_id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::RemovePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for LockPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &LockPost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can lock
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let locked = data.locked;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_locked(conn, post_id, locked)
})
.await??;
// Mod tables
let form = ModLockPostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
locked: Some(locked),
};
blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
updated_post
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::LockPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for StickyPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can sticky
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let stickied = data.stickied;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_stickied(conn, post_id, stickied)
})
.await??;
// Mod tables
let form = ModStickyPostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
stickied: Some(stickied),
};
blocking(context.pool(), move |conn| {
ModStickyPost::create(conn, &form)
})
.await??;
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::StickyPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SavePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &SavePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_saved_form = PostSavedForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
};
if data.save {
let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
if blocking(context.pool(), save).await?.is_err() {
return Err(ApiError::err("couldnt_save_post").into());
}
} else {
let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
if blocking(context.pool(), unsave).await?.is_err() {
return Err(ApiError::err("couldnt_save_post").into());
}
}
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(person_id))
})
.await??;
Ok(PostResponse { post_view })
}
}
/// Creates a post report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostReport {
type Response = CreatePostReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CreatePostReportResponse, LemmyError> {
let data: &CreatePostReport = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(ApiError::err("report_reason_required").into());
}
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let person_id = local_user_view.person.id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(&conn, post_id, None)
})
.await??;
check_community_ban(person_id, post_view.community.id, context.pool()).await?;
let report_form = PostReportForm {
creator_id: person_id,
post_id,
original_post_name: post_view.post.name,
original_post_url: post_view.post.url,
original_post_body: post_view.post.body,
reason: data.reason.to_owned(),
};
let report = match blocking(context.pool(), move |conn| {
PostReport::report(conn, &report_form)
})
.await?
{
Ok(report) => report,
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreatePostReportResponse { success: true };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreatePostReport,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::CreatePostReport,
response: report,
community_id: post_view.community.id,
websocket_id,
});
Ok(res)
}
}
/// Resolves or unresolves a post report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for ResolvePostReport {
type Response = ResolvePostReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ResolvePostReportResponse, LemmyError> {
let data: &ResolvePostReport = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let report_id = data.report_id;
let report = blocking(context.pool(), move |conn| {
PostReportView::read(&conn, report_id)
})
.await??;
let person_id = local_user_view.person.id;
is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
let resolved = data.resolved;
let resolve_fun = move |conn: &'_ _| {
if resolved {
PostReport::resolve(conn, report_id, person_id)
} else {
PostReport::unresolve(conn, report_id, person_id)
}
};
let res = ResolvePostReportResponse {
report_id,
resolved: true,
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(ApiError::err("couldnt_resolve_report").into());
};
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::ResolvePostReport,
response: res.clone(),
community_id: report.community.id,
websocket_id,
});
Ok(res)
}
}
/// Lists post reports for a community if an id is supplied
/// or returns all post reports for communities a user moderates
#[async_trait::async_trait(?Send)]
impl Perform for ListPostReports {
type Response = ListPostReportsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ListPostReportsResponse, LemmyError> {
let data: &ListPostReports = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
let posts = blocking(context.pool(), move |conn| {
PostReportQueryBuilder::create(conn)
.community_ids(community_ids)
.page(page)
.limit(limit)
.list()
})
.await??;
let res = ListPostReportsResponse { posts };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListPostReports,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
Ok(res)
}
}

View file

@ -1,97 +0,0 @@
use crate::{get_local_user_view_from_jwt, Perform};
use actix_web::web::Data;
use lemmy_api_structs::websocket::*;
use lemmy_utils::{ConnectionId, LemmyError};
use lemmy_websocket::{
messages::{JoinCommunityRoom, JoinModRoom, JoinPostRoom, JoinUserRoom},
LemmyContext,
};
#[async_trait::async_trait(?Send)]
impl Perform for UserJoin {
type Response = UserJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<UserJoinResponse, LemmyError> {
let data: &UserJoin = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinUserRoom {
local_user_id: local_user_view.local_user.id,
id: ws_id,
});
}
Ok(UserJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CommunityJoin {
type Response = CommunityJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityJoinResponse, LemmyError> {
let data: &CommunityJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinCommunityRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(CommunityJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for ModJoin {
type Response = ModJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ModJoinResponse, LemmyError> {
let data: &ModJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinModRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(ModJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for PostJoin {
type Response = PostJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostJoinResponse, LemmyError> {
let data: &PostJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinPostRoom {
post_id: data.post_id,
id: ws_id,
});
}
Ok(PostJoinResponse { joined: true })
}
}

View file

@ -1,24 +0,0 @@
[package]
name = "lemmy_api_structs"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_api_structs"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_moderator = { path = "../db_views_moderator" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_utils = { path = "../utils" }
serde = { version = "1.0.123", features = ["derive"] }
log = "0.4.14"
diesel = "1.4.5"
actix-web = "3.3.2"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
url = "2.2.1"

View file

@ -1,119 +0,0 @@
use lemmy_db_schema::{CommentId, CommunityId, LocalUserId, PostId};
use lemmy_db_views::{comment_report_view::CommentReportView, comment_view::CommentView};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct CreateComment {
pub content: String,
pub parent_id: Option<CommentId>,
pub post_id: PostId,
pub form_id: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditComment {
pub content: String,
pub comment_id: CommentId,
pub form_id: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteComment {
pub comment_id: CommentId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveComment {
pub comment_id: CommentId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkCommentAsRead {
pub comment_id: CommentId,
pub read: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SaveComment {
pub comment_id: CommentId,
pub save: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct CommentResponse {
pub comment_view: CommentView,
pub recipient_ids: Vec<LocalUserId>,
pub form_id: Option<String>, // An optional front end ID, to tell which is coming back
}
#[derive(Deserialize)]
pub struct CreateCommentLike {
pub comment_id: CommentId,
pub score: i16,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetComments {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetCommentsResponse {
pub comments: Vec<CommentView>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentReport {
pub comment_id: CommentId,
pub reason: String,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CreateCommentReportResponse {
pub success: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResolveCommentReport {
pub report_id: i32,
pub resolved: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ResolveCommentReportResponse {
// TODO this should probably return the view
pub report_id: i32,
pub resolved: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListCommentReports {
pub page: Option<i64>,
pub limit: Option<i64>,
/// if no community is given, it returns reports for all communities moderated by the auth user
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct ListCommentReportsResponse {
pub comments: Vec<CommentReportView>,
}

View file

@ -1,42 +0,0 @@
use lemmy_db_schema::{CommunityId, PostId};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct UserJoin {
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct UserJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct CommunityJoin {
pub community_id: CommunityId,
}
#[derive(Serialize, Clone)]
pub struct CommunityJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct ModJoin {
pub community_id: CommunityId,
}
#[derive(Serialize, Clone)]
pub struct ModJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct PostJoin {
pub post_id: PostId,
}
#[derive(Serialize, Clone)]
pub struct PostJoinResponse {
pub joined: bool,
}

View file

@ -1,52 +0,0 @@
[package]
name = "lemmy_apub"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_apub"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_utils = { path = "../utils" }
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_api_structs = { path = "../api_structs" }
lemmy_websocket = { path = "../websocket" }
diesel = "1.4.5"
activitystreams = "0.7.0-alpha.10"
activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.9.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false }
actix-rt = { version = "1.1.1", default-features = false }
awc = { version = "2.0.3", default-features = false }
log = "0.4.14"
rand = "0.8.3"
strum = "0.20.0"
strum_macros = "0.20.1"
lazy_static = "1.4.0"
url = { version = "2.2.1", features = ["serde"] }
percent-encoding = "2.1.0"
openssl = "0.10.32"
http = "0.2.3"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
http-signature-normalization-reqwest = { version = "0.1.3", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "0.3.6"
futures = "0.3.12"
itertools = "0.10.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
sha2 = "0.9.3"
async-trait = "0.1.42"
anyhow = "1.0.38"
thiserror = "1.0.23"
background-jobs = "0.8.0"
reqwest = { version = "0.10.10", features = ["json"] }
backtrace = "0.3.56"

View file

@ -1,2 +0,0 @@
pub(crate) mod receive;
pub(crate) mod send;

View file

@ -1,131 +0,0 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, UndoType, UpdateType},
Create,
Delete,
Undo,
Update,
},
prelude::*,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(recipient.actor_id());
send_activity_single_dest(create, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
/// Send out information about an edited private message, to the followers of the community.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(recipient.actor_id());
send_activity_single_dest(update, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
send_activity_single_dest(delete, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_undo_delete(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(recipient.actor_id());
send_activity_single_dest(undo, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_remove(&self, _mod_: &Person, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_remove(
&self,
_mod_: &Person,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
}

View file

@ -1,17 +0,0 @@
use activitystreams::{base::AnyBase, context};
use lemmy_utils::LemmyError;
use serde_json::json;
pub(crate) fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
let context_ext = AnyBase::from_arbitrary_json(json!(
{
"sc": "http://schema.org#",
"sensitive": "as:sensitive",
"stickied": "as:stickied",
"comments_enabled": {
"kind": "sc:Boolean",
"id": "pt:commentsEnabled"
}
}))?;
Ok(vec![AnyBase::from(context()), context_ext])
}

View file

@ -1,4 +0,0 @@
pub(crate) mod context;
pub(crate) mod group_extensions;
pub(crate) mod page_extension;
pub(crate) mod signatures;

View file

@ -1,145 +0,0 @@
use crate::{
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_person,
is_deleted,
should_refetch_actor,
},
inbox::person_inbox::receive_announce,
objects::FromApub,
GroupExt,
};
use activitystreams::{
actor::ApActorExt,
collection::{CollectionExt, OrderedCollection},
object::ObjectExt,
};
use anyhow::Context;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable};
use lemmy_db_schema::source::community::{Community, CommunityModerator, CommunityModeratorForm};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Get a community from its apub ID.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_community(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Community, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &apub_id_owned.into())
})
.await?;
match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id);
fetch_remote_community(apub_id, context, Some(c), recursion_counter).await
}
Ok(c) => Ok(c),
Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id);
fetch_remote_community(apub_id, context, None, recursion_counter).await
}
Err(e) => Err(e.into()),
}
}
/// Request a community by apub ID from a remote instance, including moderators. If `old_community`,
/// is set, this is an update for a community which is already known locally. If not, we don't know
/// the community yet and also pull the outbox, to get some initial posts.
async fn fetch_remote_community(
apub_id: &Url,
context: &LemmyContext,
old_community: Option<Community>,
recursion_counter: &mut i32,
) -> Result<Community, LemmyError> {
let group = fetch_remote_object::<GroupExt>(context.client(), apub_id, recursion_counter).await;
if let Some(c) = old_community.to_owned() {
if is_deleted(&group) {
blocking(context.pool(), move |conn| {
Community::update_deleted(conn, c.id, true)
})
.await??;
} else if group.is_err() {
// If fetching failed, return the existing data.
return Ok(c);
}
}
let group = group?;
let community =
Community::from_apub(&group, context, apub_id.to_owned(), recursion_counter).await?;
// Also add the community moderators too
let attributed_to = group.inner.attributed_to().context(location_info!())?;
let creator_and_moderator_uris: Vec<&Url> = attributed_to
.as_many()
.context(location_info!())?
.iter()
.map(|a| a.as_xsd_any_uri().context(""))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_person(uri, context, recursion_counter).await?;
creator_and_moderators.push(c_or_m);
}
// TODO: need to make this work to update mods of existing communities
if old_community.is_none() {
let community_id = community.id;
blocking(context.pool(), move |conn| {
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id,
person_id: mod_.id,
};
CommunityModerator::join(conn, &community_moderator_form)?;
}
Ok(()) as Result<(), LemmyError>
})
.await??;
}
// only fetch outbox for new communities, otherwise this can create an infinite loop
if old_community.is_none() {
let outbox = group.inner.outbox()?.context(location_info!())?;
fetch_community_outbox(context, outbox, &community, recursion_counter).await?
}
Ok(community)
}
async fn fetch_community_outbox(
context: &LemmyContext,
outbox: &Url,
community: &Community,
recursion_counter: &mut i32,
) -> Result<(), LemmyError> {
let outbox =
fetch_remote_object::<OrderedCollection>(context.client(), outbox, recursion_counter).await?;
let outbox_activities = outbox.items().context(location_info!())?.clone();
let mut outbox_activities = outbox_activities.many().context(location_info!())?;
if outbox_activities.len() > 20 {
outbox_activities = outbox_activities[0..20].to_vec();
}
for activity in outbox_activities {
receive_announce(context, activity, community, recursion_counter).await?;
}
Ok(())
}

View file

@ -1,83 +0,0 @@
use crate::{check_is_apub_id_valid, APUB_JSON_CONTENT_TYPE};
use anyhow::anyhow;
use lemmy_utils::{request::retry, LemmyError};
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use std::time::Duration;
use thiserror::Error;
use url::Url;
/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
/// fetch through the search).
///
/// A community fetch will load the outbox with up to 20 items, and fetch the creator for each item.
/// So we are looking at a maximum of 22 requests (rounded up just to be safe).
static MAX_REQUEST_NUMBER: i32 = 25;
#[derive(Debug, Error)]
pub(in crate::fetcher) struct FetchError {
pub inner: anyhow::Error,
pub status_code: Option<StatusCode>,
}
impl From<LemmyError> for FetchError {
fn from(t: LemmyError) -> Self {
FetchError {
inner: t.inner,
status_code: None,
}
}
}
impl From<reqwest::Error> for FetchError {
fn from(t: reqwest::Error) -> Self {
let status = t.status();
FetchError {
inner: t.into(),
status_code: status,
}
}
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self, f)
}
}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
pub(in crate::fetcher) async fn fetch_remote_object<Response>(
client: &Client,
url: &Url,
recursion_counter: &mut i32,
) -> Result<Response, FetchError>
where
Response: for<'de> Deserialize<'de> + std::fmt::Debug,
{
*recursion_counter += 1;
if *recursion_counter > MAX_REQUEST_NUMBER {
return Err(LemmyError::from(anyhow!("Maximum recursion depth reached")).into());
}
check_is_apub_id_valid(&url)?;
let timeout = Duration::from_secs(60);
let res = retry(|| {
client
.get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE)
.timeout(timeout)
.send()
})
.await?;
if res.status() == StatusCode::GONE {
return Err(FetchError {
inner: anyhow!("Remote object {} was deleted", url),
status_code: Some(res.status()),
});
}
Ok(res.json().await?)
}

View file

@ -1,72 +0,0 @@
pub(crate) mod community;
mod fetch;
pub(crate) mod objects;
pub(crate) mod person;
pub mod search;
use crate::{
fetcher::{
community::get_or_fetch_and_upsert_community,
fetch::FetchError,
person::get_or_fetch_and_upsert_person,
},
ActorType,
};
use chrono::NaiveDateTime;
use http::StatusCode;
use lemmy_db_schema::naive_now;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
use url::Url;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
fn is_deleted<Response>(fetch_response: &Result<Response, FetchError>) -> bool
where
Response: for<'de> Deserialize<'de>,
{
if let Err(e) = fetch_response {
if let Some(status) = e.status_code {
if status == StatusCode::GONE {
return true;
}
}
}
false
}
/// Get a remote actor from its apub ID (either a person or a community). Thin wrapper around
/// `get_or_fetch_and_upsert_person()` and `get_or_fetch_and_upsert_community()`.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_actor(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Box<dyn ActorType>, LemmyError> {
let community = get_or_fetch_and_upsert_community(apub_id, context, recursion_counter).await;
let actor: Box<dyn ActorType> = match community {
Ok(c) => Box::new(c),
Err(_) => Box::new(get_or_fetch_and_upsert_person(apub_id, context, recursion_counter).await?),
};
Ok(actor)
}
/// Determines when a remote actor should be refetched from its instance. In release builds, this is
/// `ACTOR_REFETCH_INTERVAL_SECONDS` after the last refetch, in debug builds
/// `ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG`.
///
/// TODO it won't pick up new avatars, summaries etc until a day after.
/// Actors need an "update" activity pushed to other servers to fix this.
fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
let update_interval = if cfg!(debug_assertions) {
// avoid infinite loop when fetching community outbox
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
} else {
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
};
last_refreshed.lt(&(naive_now() - update_interval))
}

View file

@ -1,83 +0,0 @@
use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, Crud};
use lemmy_db_schema::source::{comment::Comment, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Gets a post by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community is also pulled if necessary. Comments are not pulled.
pub(crate) async fn get_or_fetch_and_insert_post(
post_ap_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Post, LemmyError> {
let post_ap_id_owned = post_ap_id.to_owned();
let post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &post_ap_id_owned.into())
})
.await?;
match post {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let page =
fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub(&page, context, post_ap_id.to_owned(), recursion_counter).await?;
Ok(post)
}
Err(e) => Err(e.into()),
}
}
/// Gets a comment by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community, post and comment are also pulled if necessary.
pub(crate) async fn get_or_fetch_and_insert_comment(
comment_ap_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Comment, LemmyError> {
let comment_ap_id_owned = comment_ap_id.to_owned();
let comment = blocking(context.pool(), move |conn| {
Comment::read_from_apub_id(conn, &comment_ap_id_owned.into())
})
.await?;
match comment {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!(
"Fetching and creating remote comment and its parents: {}",
comment_ap_id
);
let comment =
fetch_remote_object::<NoteExt>(context.client(), comment_ap_id, recursion_counter).await?;
let comment = Comment::from_apub(
&comment,
context,
comment_ap_id.to_owned(),
recursion_counter,
)
.await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
if post.locked {
return Err(anyhow!("Post is locked").into());
}
Ok(comment)
}
Err(e) => Err(e.into()),
}
}

View file

@ -1,73 +0,0 @@
use crate::{
fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor},
objects::FromApub,
PersonExt,
};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::person::Person_, ApubObject};
use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Get a person from its apub ID.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_person(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Person, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let person = blocking(context.pool(), move |conn| {
Person::read_from_apub_id(conn, &apub_id_owned.into())
})
.await?;
match person {
// If its older than a day, re-fetch it
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await;
if is_deleted(&person) {
// TODO: use Person::update_deleted() once implemented
blocking(context.pool(), move |conn| {
Person::delete_account(conn, u.id)
})
.await??;
return Err(anyhow!("Person was deleted by remote instance").into());
} else if person.is_err() {
return Ok(u);
}
let person =
Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?;
let person_id = person.id;
blocking(context.pool(), move |conn| {
Person::mark_as_updated(conn, person_id)
})
.await??;
Ok(person)
}
Ok(u) => Ok(u),
Err(NotFound {}) => {
debug!("Fetching and creating remote person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
let person =
Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?;
Ok(person)
}
Err(e) => Err(e.into()),
}
}

View file

@ -1,206 +0,0 @@
use crate::{
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_community,
get_or_fetch_and_upsert_person,
is_deleted,
},
find_object_by_id,
objects::FromApub,
GroupExt,
NoteExt,
Object,
PageExt,
PersonExt,
};
use activitystreams::base::BaseExt;
use anyhow::{anyhow, Context};
use lemmy_api_structs::{blocking, site::SearchResponse};
use lemmy_db_queries::{
source::{
comment::Comment_,
community::Community_,
person::Person_,
post::Post_,
private_message::PrivateMessage_,
},
SearchType,
};
use lemmy_db_schema::source::{
comment::Comment,
community::Community,
person::Person,
post::Post,
private_message::PrivateMessage,
};
use lemmy_db_views::{comment_view::CommentView, post_view::PostView};
use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
Page(Box<PageExt>),
Comment(Box<NoteExt>),
}
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
///
/// Some working examples for use with the `docker/federation/` setup:
/// http://lemmy_alpha:8541/c/main, or !main@lemmy_alpha:8541
/// http://lemmy_beta:8551/u/lemmy_alpha, or @lemmy_beta@lemmy_beta:8551
/// http://lemmy_gamma:8561/post/3
/// http://lemmy_delta:8571/comment/2
pub async fn search_by_apub_id(
query: &str,
context: &LemmyContext,
) -> Result<SearchResponse, LemmyError> {
// Parse the shorthand query url
let query_url = if query.contains('@') {
debug!("Search for {}", query);
let split = query.split('@').collect::<Vec<&str>>();
// Person type will look like ['', username, instance]
// Community will look like [!community, instance]
let (name, instance) = if split.len() == 3 {
(format!("/u/{}", split[1]), split[2])
} else if split.len() == 2 {
if split[0].contains('!') {
let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1])
} else {
return Err(anyhow!("Invalid search query: {}", query).into());
}
} else {
return Err(anyhow!("Invalid search query: {}", query).into());
};
let url = format!(
"{}://{}{}",
Settings::get().get_protocol_string(),
instance,
name
);
Url::parse(&url)?
} else {
Url::parse(&query)?
};
let recursion_counter = &mut 0;
let fetch_response =
fetch_remote_object::<SearchAcceptedObjects>(context.client(), &query_url, recursion_counter)
.await;
if is_deleted(&fetch_response) {
delete_object_locally(&query_url, context).await?;
}
// Necessary because we get a stack overflow using FetchError
let fet_res = fetch_response.map_err(|e| LemmyError::from(e.inner))?;
build_response(fet_res, query_url, recursion_counter, context).await
}
async fn build_response(
fetch_response: SearchAcceptedObjects,
query_url: Url,
recursion_counter: &mut i32,
context: &LemmyContext,
) -> Result<SearchResponse, LemmyError> {
let domain = query_url.domain().context("url has no domain")?;
let mut response = SearchResponse {
type_: SearchType::All.to_string(),
comments: vec![],
posts: vec![],
communities: vec![],
users: vec![],
};
match fetch_response {
SearchAcceptedObjects::Person(p) => {
let person_uri = p.inner.id(domain)?.context("person has no id")?;
let person = get_or_fetch_and_upsert_person(&person_uri, context, recursion_counter).await?;
response.users = vec![
blocking(context.pool(), move |conn| {
PersonViewSafe::read(conn, person.id)
})
.await??,
];
}
SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.id(domain)?.context("group has no id")?;
let community =
get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?;
response.communities = vec![
blocking(context.pool(), move |conn| {
CommunityView::read(conn, community.id, None)
})
.await??,
];
}
SearchAcceptedObjects::Page(p) => {
let p = Post::from_apub(&p, context, query_url, recursion_counter).await?;
response.posts =
vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
}
SearchAcceptedObjects::Comment(c) => {
let c = Comment::from_apub(&c, context, query_url, recursion_counter).await?;
response.comments = vec![
blocking(context.pool(), move |conn| {
CommentView::read(conn, c.id, None)
})
.await??,
];
}
};
Ok(response)
}
async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> {
let res = find_object_by_id(context, query_url.to_owned()).await?;
match res {
Object::Comment(c) => {
blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, c.id, true)
})
.await??;
}
Object::Post(p) => {
blocking(context.pool(), move |conn| {
Post::update_deleted(conn, p.id, true)
})
.await??;
}
Object::Person(u) => {
// TODO: implement update_deleted() for user, move it to ApubObject trait
blocking(context.pool(), move |conn| {
Person::delete_account(conn, u.id)
})
.await??;
}
Object::Community(c) => {
blocking(context.pool(), move |conn| {
Community::update_deleted(conn, c.id, true)
})
.await??;
}
Object::PrivateMessage(pm) => {
blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, pm.id, true)
})
.await??;
}
}
Err(anyhow!("Object was deleted").into())
}

View file

@ -1,78 +0,0 @@
use crate::{
extensions::context::lemmy_context,
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
ActorType,
};
use activitystreams::{
base::BaseExt,
collection::{CollectionExt, OrderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::person::Person_;
use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)]
pub struct PersonQuery {
user_name: String,
}
/// Return the ActivityPub json representation of a local person over HTTP.
pub async fn get_apub_person_http(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name;
// TODO: this needs to be able to read deleted persons, so that it can send tombstones
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(conn, &user_name)
})
.await??;
if !person.deleted {
let apub = person.to_apub(context.pool()).await?;
Ok(create_apub_response(&apub))
} else {
Ok(create_apub_tombstone_response(&person.to_tombstone()?))
}
}
pub async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(&conn, &info.user_name)
})
.await??;
// TODO: populate the person outbox
let mut collection = OrderedCollection::new();
collection
.set_many_items(Vec::<Url>::new())
.set_many_contexts(lemmy_context()?)
.set_id(person.get_outbox_url()?)
.set_total_items(0_u64);
Ok(create_apub_response(&collection))
}
pub async fn get_apub_person_inbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(&conn, &info.user_name)
})
.await??;
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", person.actor_id.into_inner()).parse()?)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View file

@ -1,379 +0,0 @@
#[macro_use]
extern crate lazy_static;
pub mod activities;
pub mod activity_queue;
pub mod extensions;
pub mod fetcher;
pub mod http;
pub mod inbox;
pub mod objects;
pub mod routes;
use crate::extensions::{
group_extensions::GroupExtension,
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
};
use activitystreams::{
activity::Follow,
actor::{ApActor, Group, Person},
base::AnyBase,
object::{ApObject, Note, Page},
};
use activitystreams_ext::{Ext1, Ext2};
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
use lemmy_db_schema::{
source::{
activity::Activity,
comment::Comment,
community::Community,
person::Person as DbPerson,
post::Post,
private_message::PrivateMessage,
},
DbUrl,
};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::Serialize;
use std::net::IpAddr;
use url::{ParseError, Url};
/// Activitystreams type for community
type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for person
type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
/// Activitystreams type for post
type PageExt = Ext1<ApObject<Page>, PageExtension>;
type NoteExt = ApObject<Note>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
/// Checks if the ID is allowed for sending or receiving.
///
/// In particular, it checks for:
/// - federation being enabled (if its disabled, only local URLs are allowed)
/// - the correct scheme (either http or https)
/// - URL being in the allowlist (if it is active)
/// - URL not being in the blocklist (if it is active)
///
/// Note that only one of allowlist and blacklist can be enabled, not both.
fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
let settings = Settings::get();
let domain = apub_id.domain().context(location_info!())?.to_string();
let local_instance = settings.get_hostname_without_port()?;
if !settings.federation().enabled {
return if domain == local_instance {
Ok(())
} else {
Err(
anyhow!(
"Trying to connect with {}, but federation is disabled",
domain
)
.into(),
)
};
}
let host = apub_id.host_str().context(location_info!())?;
let host_as_ip = host.parse::<IpAddr>();
if host == "localhost" || host_as_ip.is_ok() {
return Err(anyhow!("invalid hostname {}: {}", host, apub_id).into());
}
if apub_id.scheme() != Settings::get().get_protocol_string() {
return Err(anyhow!("invalid apub id scheme {}: {}", apub_id.scheme(), apub_id).into());
}
let allowed_instances = Settings::get().get_allowed_instances();
let blocked_instances = Settings::get().get_blocked_instances();
if allowed_instances.is_none() && blocked_instances.is_none() {
Ok(())
} else if let Some(mut allowed) = allowed_instances {
// need to allow this explicitly because apub receive might contain objects from our local
// instance. split is needed to remove the port in our federation test setup.
allowed.push(local_instance);
if allowed.contains(&domain) {
Ok(())
} else {
Err(anyhow!("{} not in federation allowlist", domain).into())
}
} else if let Some(blocked) = blocked_instances {
if blocked.contains(&domain) {
Err(anyhow!("{} is in federation blocklist", domain).into())
} else {
Ok(())
}
} else {
panic!("Invalid config, both allowed_instances and blocked_instances are specified");
}
}
/// Common functions for ActivityPub objects, which are implemented by most (but not all) objects
/// and actors in Lemmy.
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType {
async fn send_create(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_update(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_delete(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_remove(&self, mod_: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(&self, creator: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
/// implemented by all actors.
#[async_trait::async_trait(?Send)]
pub trait ActorType {
fn is_local(&self) -> bool;
fn actor_id(&self) -> Url;
// TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
fn public_key(&self) -> Option<String>;
fn private_key(&self) -> Option<String>;
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
fn get_shared_inbox_or_inbox_url(&self) -> Url;
/// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
/// local actors).
fn get_outbox_url(&self) -> Result<Url, LemmyError> {
if !self.is_local() {
return Err(anyhow!("get_outbox_url() called for remote actor").into());
}
Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
}
fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
Ok(
PublicKey {
id: format!("{}#main-key", self.actor_id()),
owner: self.actor_id(),
public_key_pem: self.public_key().context(location_info!())?,
}
.to_ext(),
)
}
}
pub enum EndpointType {
Community,
Person,
Post,
Comment,
PrivateMessage,
}
/// Generates the ActivityPub ID for a given object type and ID.
pub fn generate_apub_endpoint(
endpoint_type: EndpointType,
name: &str,
) -> Result<DbUrl, ParseError> {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::Person => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Ok(
Url::parse(&format!(
"{}/{}/{}",
Settings::get().get_protocol_and_hostname(),
point,
name
))?
.into(),
)
}
pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{}/followers", actor_id))?.into())
}
pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
}
pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
let actor_id = actor_id.clone().into_inner();
let url = format!(
"{}://{}{}/inbox",
&actor_id.scheme(),
&actor_id.host_str().context(location_info!())?,
if let Some(port) = actor_id.port() {
format!(":{}", port)
} else {
"".to_string()
},
);
Ok(Url::parse(&url)?.into())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
pub(crate) async fn insert_activity<T>(
ap_id: &Url,
activity: T,
local: bool,
sensitive: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + std::fmt::Debug + Send + 'static,
{
let ap_id = ap_id.to_owned().into();
blocking(pool, move |conn| {
Activity::insert(conn, ap_id, &activity, local, sensitive)
})
.await??;
Ok(())
}
pub(crate) enum PostOrComment {
Comment(Box<Comment>),
Post(Box<Post>),
}
/// Tries to find a post or comment in the local database, without any network requests.
/// This is used to handle deletions and removals, because in case we dont have the object, we can
/// simply ignore the activity.
pub(crate) async fn find_post_or_comment_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<PostOrComment, LemmyError> {
let ap_id = apub_id.clone();
let post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = post {
return Ok(PostOrComment::Post(Box::new(p)));
}
let ap_id = apub_id.clone();
let comment = blocking(context.pool(), move |conn| {
Comment::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = comment {
return Ok(PostOrComment::Comment(Box::new(c)));
}
Err(NotFound.into())
}
pub(crate) enum Object {
Comment(Box<Comment>),
Post(Box<Post>),
Community(Box<Community>),
Person(Box<DbPerson>),
PrivateMessage(Box<PrivateMessage>),
}
pub(crate) async fn find_object_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<Object, LemmyError> {
let ap_id = apub_id.clone();
if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
return Ok(match pc {
PostOrComment::Post(p) => Object::Post(Box::new(*p)),
PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
});
}
let ap_id = apub_id.clone();
let person = blocking(context.pool(), move |conn| {
DbPerson::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(u) = person {
return Ok(Object::Person(Box::new(u)));
}
let ap_id = apub_id.clone();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = community {
return Ok(Object::Community(Box::new(c)));
}
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &apub_id.into())
})
.await?;
if let Ok(pm) = private_message {
return Ok(Object::PrivateMessage(Box::new(pm)));
}
Err(NotFound.into())
}

View file

@ -1,247 +0,0 @@
use crate::{
check_is_apub_id_valid,
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
inbox::community_inbox::check_community_or_site_ban,
};
use activitystreams::{
base::{AsBase, BaseExt, ExtendsExt},
markers::Base,
mime::{FromStrError, Mime},
object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
};
use anyhow::{anyhow, Context};
use chrono::NaiveDateTime;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::{source::community::Community, CommunityId, DbUrl};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{convert_datetime, markdown_to_html},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
/// Trait for converting an object or actor into the respective ActivityPub type.
#[async_trait::async_trait(?Send)]
pub(crate) trait ToApub {
type ApubType;
async fn to_apub(&self, pool: &DbPool) -> Result<Self::ApubType, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub(crate) trait FromApub {
type ApubType;
/// Converts an object from ActivityPub type to Lemmy internal type.
///
/// * `apub` The object to read from
/// * `context` LemmyContext which holds DB pool, HTTP client etc
/// * `expected_domain` Domain where the object was received from
async fn from_apub(
apub: &Self::ApubType,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
#[async_trait::async_trait(?Send)]
pub(in crate::objects) trait FromApubToForm<ApubType> {
async fn from_apub(
apub: &ApubType,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
object_id: Url,
updated: Option<NaiveDateTime>,
former_type: T,
) -> Result<Tombstone, LemmyError>
where
T: ToString,
{
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::new();
tombstone.set_id(object_id);
tombstone.set_former_type(former_type.to_string());
tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone)
} else {
Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
}
} else {
Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
pub(in crate::objects) fn check_object_domain<T, Kind>(
apub: &T,
expected_domain: Url,
) -> Result<DbUrl, LemmyError>
where
T: Base + AsBase<Kind>,
{
let domain = expected_domain.domain().context(location_info!())?;
let object_id = apub.id(domain)?.context(location_info!())?;
check_is_apub_id_valid(object_id)?;
Ok(object_id.to_owned().into())
}
pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
object: &mut T,
markdown_text: &str,
) -> Result<(), LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
let mut source = Object::<()>::new_none_type();
source
.set_content(markdown_text)
.set_media_type(mime_markdown()?);
object.set_source(source.into_any_base()?);
object.set_content(markdown_to_html(markdown_text));
object.set_media_type(mime_html()?);
Ok(())
}
pub(in crate::objects) fn get_source_markdown_value<T, Kind1, Kind2>(
object: &T,
) -> Result<Option<String>, LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
let content = object
.content()
.map(|s| s.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
if content.is_some() {
let source = object.source().context(location_info!())?;
let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
check_is_markdown(source.media_type())?;
let source_content = source
.content()
.map(|s| s.as_single_xsd_string())
.flatten()
.context(location_info!())?
.to_string();
return Ok(Some(source_content));
}
Ok(None)
}
fn mime_markdown() -> Result<Mime, FromStrError> {
"text/markdown".parse()
}
fn mime_html() -> Result<Mime, FromStrError> {
"text/html".parse()
}
pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> {
let mime = mime.context(location_info!())?;
if !mime.eq(&mime_markdown()?) {
Err(LemmyError::from(anyhow!(
"Lemmy only supports markdown content"
)))
} else {
Ok(())
}
}
/// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object
/// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise
/// the apub object is parsed, inserted and returned.
pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
from: &From,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<To, LemmyError>
where
From: BaseExt<Kind>,
To: ApubObject<ToForm> + Crud<ToForm, IdType> + Send + 'static,
ToForm: FromApubToForm<From> + Send + 'static,
{
let object_id = from.id_unchecked().context(location_info!())?.to_owned();
let domain = object_id.domain().context(location_info!())?;
// if its a local object, return it directly from the database
if Settings::get().hostname() == domain {
let object = blocking(context.pool(), move |conn| {
To::read_from_apub_id(conn, &object_id.into())
})
.await??;
Ok(object)
}
// otherwise parse and insert, assuring that it comes from the right domain
else {
let to_form = ToForm::from_apub(&from, context, expected_domain, request_counter).await?;
let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
Ok(to)
}
}
pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
object: &T,
community_id: CommunityId,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: ObjectExt<Kind>,
{
let person_id = object
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community_id, context.pool()).await
}
pub(in crate::objects) async fn get_to_community<T, Kind>(
object: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError>
where
T: ObjectExt<Kind>,
{
let community_ids = object
.to()
.context(location_info!())?
.as_many()
.context(location_info!())?
.iter()
.map(|a| a.as_xsd_any_uri().context(location_info!()))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
for cid in community_ids {
let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
if community.is_ok() {
return community;
}
}
Err(NotFound.into())
}

View file

@ -1,127 +0,0 @@
use crate::{
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
};
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
};
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{
person::Person,
private_message::{PrivateMessage, PrivateMessageForm},
};
use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut private_message = ApObject::new(Note::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
private_message
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(recipient.actor_id.into_inner())
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut private_message, &self.content)?;
if let Some(u) = self.updated {
private_message.set_updated(convert_datetime(u));
}
Ok(private_message)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
NoteType::Note,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessage {
type ApubType = NoteExt;
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PrivateMessage, LemmyError> {
get_object_from_apub(note, context, expected_domain, request_counter).await
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for PrivateMessageForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note
.attributed_to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).await?;
let recipient_actor_id = note
.to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
let recipient =
get_or_fetch_and_upsert_person(&recipient_actor_id, context, request_counter).await?;
let ap_id = note.id_unchecked().context(location_info!())?.to_string();
check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
let content = get_source_markdown_value(note)?.context(location_info!())?;
Ok(PrivateMessageForm {
creator_id: creator.id,
recipient_id: recipient.id,
content,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
ap_id: Some(check_object_domain(note, expected_domain)?),
local: false,
})
}
}

View file

@ -1,80 +0,0 @@
use crate::{
http::{
comment::get_apub_comment,
community::{
get_apub_community_followers,
get_apub_community_http,
get_apub_community_inbox,
get_apub_community_outbox,
},
get_activity,
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
post::get_apub_post,
},
inbox::{
community_inbox::community_inbox,
person_inbox::person_inbox,
shared_inbox::shared_inbox,
},
APUB_JSON_CONTENT_TYPE,
};
use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use lemmy_utils::settings::structs::Settings;
use sha2::{Digest, Sha256};
static APUB_JSON_CONTENT_TYPE_LONG: &str =
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation().enabled {
println!("federation enabled, host is {}", Settings::get().hostname());
let digest_verifier = VerifyDigest::new(Sha256::new());
let header_guard_accept = guard::Any(guard::Header("Accept", APUB_JSON_CONTENT_TYPE))
.or(guard::Header("Accept", APUB_JSON_CONTENT_TYPE_LONG));
let header_guard_content_type =
guard::Any(guard::Header("Content-Type", APUB_JSON_CONTENT_TYPE))
.or(guard::Header("Content-Type", APUB_JSON_CONTENT_TYPE_LONG));
cfg
.service(
web::scope("/")
.guard(header_guard_accept)
.route(
"/c/{community_name}",
web::get().to(get_apub_community_http),
)
.route(
"/c/{community_name}/followers",
web::get().to(get_apub_community_followers),
)
.route(
"/c/{community_name}/outbox",
web::get().to(get_apub_community_outbox),
)
.route(
"/c/{community_name}/inbox",
web::get().to(get_apub_community_inbox),
)
.route("/u/{user_name}", web::get().to(get_apub_person_http))
.route(
"/u/{user_name}/outbox",
web::get().to(get_apub_person_outbox),
)
.route("/u/{user_name}/inbox", web::get().to(get_apub_person_inbox))
.route("/post/{post_id}", web::get().to(get_apub_post))
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
.route("/activities/{type_}/{id}", web::get().to(get_activity)),
)
// Inboxes dont work with the header guard for some reason.
.service(
web::scope("/")
.wrap(digest_verifier)
.guard(header_guard_content_type)
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(person_inbox))
.route("/inbox", web::post().to(shared_inbox)),
);
}
}

View file

@ -1,29 +0,0 @@
[package]
name = "lemmy_db_queries"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_db_queries"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_utils = { path = "../utils" }
lemmy_db_schema = { path = "../db_schema" }
diesel = { version = "1.4.5", features = ["postgres","chrono","r2d2","serde_json"] }
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.123", features = ["derive"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
strum = "0.20.0"
strum_macros = "0.20.1"
log = "0.4.14"
sha2 = "0.9.3"
url = { version = "2.2.1", features = ["serde"] }
lazy_static = "1.4.0"
regex = "1.4.3"
bcrypt = "0.9.0"
[dev-dependencies]
serial_test = "0.5.1"

View file

@ -1,216 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_schema::{schema::comment_aggregates, CommentId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "comment_aggregates"]
pub struct CommentAggregates {
pub id: i32,
pub comment_id: CommentId,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub published: chrono::NaiveDateTime,
}
impl CommentAggregates {
pub fn read(conn: &PgConnection, comment_id: CommentId) -> Result<Self, Error> {
comment_aggregates::table
.filter(comment_aggregates::comment_id.eq(comment_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
aggregates::comment_aggregates::CommentAggregates,
establish_unpooled_connection,
Crud,
Likeable,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "thommy_comment_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_person = PersonForm {
name: "jerry_comment_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_comment_agg".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
nsfw: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: Some(inserted_comment.id),
published: None,
updated: None,
ap_id: None,
local: true,
};
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
let comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
CommentLike::like(&conn, &comment_like).unwrap();
let comment_aggs_before_delete = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
assert_eq!(1, comment_aggs_before_delete.score);
assert_eq!(1, comment_aggs_before_delete.upvotes);
assert_eq!(0, comment_aggs_before_delete.downvotes);
// Add a post dislike from the other person
let comment_dislike = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: another_inserted_person.id,
score: -1,
};
CommentLike::like(&conn, &comment_dislike).unwrap();
let comment_aggs_after_dislike = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
assert_eq!(0, comment_aggs_after_dislike.score);
assert_eq!(1, comment_aggs_after_dislike.upvotes);
assert_eq!(1, comment_aggs_after_dislike.downvotes);
// Remove the first comment like
CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
let after_like_remove = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
assert_eq!(-1, after_like_remove.score);
assert_eq!(0, after_like_remove.upvotes);
assert_eq!(1, after_like_remove.downvotes);
// Remove the parent post
Post::delete(&conn, inserted_post.id).unwrap();
// Should be none found, since the post was deleted
let after_delete = CommentAggregates::read(&conn, inserted_comment.id);
assert!(after_delete.is_err());
// This should delete all the associated rows, and fire triggers
Person::delete(&conn, another_inserted_person.id).unwrap();
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
}
}

View file

@ -1,261 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_schema::{schema::community_aggregates, CommunityId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "community_aggregates"]
pub struct CommunityAggregates {
pub id: i32,
pub community_id: CommunityId,
pub subscribers: i64,
pub posts: i64,
pub comments: i64,
pub published: chrono::NaiveDateTime,
pub users_active_day: i64,
pub users_active_week: i64,
pub users_active_month: i64,
pub users_active_half_year: i64,
}
impl CommunityAggregates {
pub fn read(conn: &PgConnection, community_id: CommunityId) -> Result<Self, Error> {
community_aggregates::table
.filter(community_aggregates::community_id.eq(community_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
aggregates::community_aggregates::CommunityAggregates,
establish_unpooled_connection,
Crud,
Followable,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "thommy_community_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_person = PersonForm {
name: "jerry_community_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_community_agg".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let another_community = CommunityForm {
name: "TIL_community_agg_2".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_community = Community::create(&conn, &another_community).unwrap();
let first_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
pending: false,
};
CommunityFollower::follow(&conn, &first_person_follow).unwrap();
let second_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: another_inserted_person.id,
pending: false,
};
CommunityFollower::follow(&conn, &second_person_follow).unwrap();
let another_community_follow = CommunityFollowerForm {
community_id: another_inserted_community.id,
person_id: inserted_person.id,
pending: false,
};
CommunityFollower::follow(&conn, &another_community_follow).unwrap();
let new_post = PostForm {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
nsfw: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: Some(inserted_comment.id),
published: None,
updated: None,
ap_id: None,
local: true,
};
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
let community_aggregates_before_delete =
CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(2, community_aggregates_before_delete.subscribers);
assert_eq!(1, community_aggregates_before_delete.posts);
assert_eq!(2, community_aggregates_before_delete.comments);
// Test the other community
let another_community_aggs =
CommunityAggregates::read(&conn, another_inserted_community.id).unwrap();
assert_eq!(1, another_community_aggs.subscribers);
assert_eq!(0, another_community_aggs.posts);
assert_eq!(0, another_community_aggs.comments);
// Unfollow test
CommunityFollower::unfollow(&conn, &second_person_follow).unwrap();
let after_unfollow = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(1, after_unfollow.subscribers);
// Follow again just for the later tests
CommunityFollower::follow(&conn, &second_person_follow).unwrap();
let after_follow_again = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(2, after_follow_again.subscribers);
// Remove a parent comment (the comment count should also be 0)
Post::delete(&conn, inserted_post.id).unwrap();
let after_parent_post_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(0, after_parent_post_delete.comments);
assert_eq!(0, after_parent_post_delete.posts);
// Remove the 2nd person
Person::delete(&conn, another_inserted_person.id).unwrap();
let after_person_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(1, after_person_delete.subscribers);
// This should delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = CommunityAggregates::read(&conn, inserted_community.id);
assert!(after_delete.is_err());
}
}

View file

@ -1,5 +0,0 @@
pub mod comment_aggregates;
pub mod community_aggregates;
pub mod person_aggregates;
pub mod post_aggregates;
pub mod site_aggregates;

View file

@ -1,237 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_schema::{schema::person_aggregates, PersonId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "person_aggregates"]
pub struct PersonAggregates {
pub id: i32,
pub person_id: PersonId,
pub post_count: i64,
pub post_score: i64,
pub comment_count: i64,
pub comment_score: i64,
}
impl PersonAggregates {
pub fn read(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
person_aggregates::table
.filter(person_aggregates::person_id.eq(person_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
aggregates::person_aggregates::PersonAggregates,
establish_unpooled_connection,
Crud,
Likeable,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm, PostLike, PostLikeForm},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "thommy_user_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_person = PersonForm {
name: "jerry_user_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_site_agg".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
nsfw: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let _inserted_post_like = PostLike::like(&conn, &post_like).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let mut comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
person_id: inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap();
let mut child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: Some(inserted_comment.id),
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
let child_comment_like = CommentLikeForm {
comment_id: inserted_child_comment.id,
person_id: another_inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
let _inserted_child_comment_like = CommentLike::like(&conn, &child_comment_like).unwrap();
let person_aggregates_before_delete =
PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_aggregates_before_delete.post_count);
assert_eq!(1, person_aggregates_before_delete.post_score);
assert_eq!(2, person_aggregates_before_delete.comment_count);
assert_eq!(2, person_aggregates_before_delete.comment_score);
// Remove a post like
PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
let after_post_like_remove = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_post_like_remove.post_score);
// Remove a parent comment (the scores should also be removed)
Comment::delete(&conn, inserted_comment.id).unwrap();
let after_parent_comment_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_parent_comment_delete.comment_count);
assert_eq!(0, after_parent_comment_delete.comment_score);
// Add in the two comments again, then delete the post.
let new_parent_comment = Comment::create(&conn, &comment_form).unwrap();
child_comment_form.parent_id = Some(new_parent_comment.id);
Comment::create(&conn, &child_comment_form).unwrap();
comment_like.comment_id = new_parent_comment.id;
CommentLike::like(&conn, &comment_like).unwrap();
let after_comment_add = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(2, after_comment_add.comment_count);
assert_eq!(1, after_comment_add.comment_score);
Post::delete(&conn, inserted_post.id).unwrap();
let after_post_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_post_delete.comment_score);
assert_eq!(0, after_post_delete.comment_count);
assert_eq!(0, after_post_delete.post_score);
assert_eq!(0, after_post_delete.post_count);
// This should delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
Person::delete(&conn, another_inserted_person.id).unwrap();
// Should be none found
let after_delete = PersonAggregates::read(&conn, inserted_person.id);
assert!(after_delete.is_err());
}
}

View file

@ -1,226 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_schema::{schema::post_aggregates, PostId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "post_aggregates"]
pub struct PostAggregates {
pub id: i32,
pub post_id: PostId,
pub comments: i64,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub stickied: bool,
pub published: chrono::NaiveDateTime,
pub newest_comment_time_necro: chrono::NaiveDateTime, // A newest comment time, limited to 2 days, to prevent necrobumping
pub newest_comment_time: chrono::NaiveDateTime,
}
impl PostAggregates {
pub fn read(conn: &PgConnection, post_id: PostId) -> Result<Self, Error> {
post_aggregates::table
.filter(post_aggregates::post_id.eq(post_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
aggregates::post_aggregates::PostAggregates,
establish_unpooled_connection,
Crud,
Likeable,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm, PostLike, PostLikeForm},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "thommy_community_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_person = PersonForm {
name: "jerry_community_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_community_agg".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
nsfw: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: Some(inserted_comment.id),
published: None,
updated: None,
ap_id: None,
local: true,
};
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
PostLike::like(&conn, &post_like).unwrap();
let post_aggs_before_delete = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(2, post_aggs_before_delete.comments);
assert_eq!(1, post_aggs_before_delete.score);
assert_eq!(1, post_aggs_before_delete.upvotes);
assert_eq!(0, post_aggs_before_delete.downvotes);
// Add a post dislike from the other person
let post_dislike = PostLikeForm {
post_id: inserted_post.id,
person_id: another_inserted_person.id,
score: -1,
};
PostLike::like(&conn, &post_dislike).unwrap();
let post_aggs_after_dislike = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(2, post_aggs_after_dislike.comments);
assert_eq!(0, post_aggs_after_dislike.score);
assert_eq!(1, post_aggs_after_dislike.upvotes);
assert_eq!(1, post_aggs_after_dislike.downvotes);
// Remove the parent comment
Comment::delete(&conn, inserted_comment.id).unwrap();
let after_comment_delete = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(0, after_comment_delete.comments);
assert_eq!(0, after_comment_delete.score);
assert_eq!(1, after_comment_delete.upvotes);
assert_eq!(1, after_comment_delete.downvotes);
// Remove the first post like
PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
let after_like_remove = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(0, after_like_remove.comments);
assert_eq!(-1, after_like_remove.score);
assert_eq!(0, after_like_remove.upvotes);
assert_eq!(1, after_like_remove.downvotes);
// This should delete all the associated rows, and fire triggers
Person::delete(&conn, another_inserted_person.id).unwrap();
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = PostAggregates::read(&conn, inserted_post.id);
assert!(after_delete.is_err());
}
}

View file

@ -1,180 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_schema::schema::site_aggregates;
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "site_aggregates"]
pub struct SiteAggregates {
pub id: i32,
pub site_id: i32,
pub users: i64,
pub posts: i64,
pub comments: i64,
pub communities: i64,
pub users_active_day: i64,
pub users_active_week: i64,
pub users_active_month: i64,
pub users_active_half_year: i64,
}
impl SiteAggregates {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
site_aggregates::table.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{aggregates::site_aggregates::SiteAggregates, establish_unpooled_connection, Crud};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
site::{Site, SiteForm},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "thommy_site_agg".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let site_form = SiteForm {
name: "test_site".into(),
description: None,
icon: None,
banner: None,
creator_id: inserted_person.id,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
updated: None,
};
Site::create(&conn, &site_form).unwrap();
let new_community = CommunityForm {
name: "TIL_site_agg".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
nsfw: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
// Insert two of those posts
let inserted_post = Post::create(&conn, &new_post).unwrap();
let _inserted_post_again = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
// Insert two of those comments
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: Some(inserted_comment.id),
published: None,
updated: None,
ap_id: None,
local: true,
};
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
let site_aggregates_before_delete = SiteAggregates::read(&conn).unwrap();
assert_eq!(1, site_aggregates_before_delete.users);
assert_eq!(1, site_aggregates_before_delete.communities);
assert_eq!(2, site_aggregates_before_delete.posts);
assert_eq!(2, site_aggregates_before_delete.comments);
// Try a post delete
Post::delete(&conn, inserted_post.id).unwrap();
let site_aggregates_after_post_delete = SiteAggregates::read(&conn).unwrap();
assert_eq!(1, site_aggregates_after_post_delete.posts);
assert_eq!(0, site_aggregates_after_post_delete.comments);
// This shouuld delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
let after_delete = SiteAggregates::read(&conn);
assert!(after_delete.is_err());
}
}

View file

@ -1,316 +0,0 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate strum_macros;
#[macro_use]
extern crate lazy_static;
// this is used in tests
#[allow(unused_imports)]
#[macro_use]
extern crate diesel_migrations;
#[cfg(test)]
extern crate serial_test;
use diesel::{result::Error, *};
use lemmy_db_schema::{CommunityId, DbUrl, PersonId};
use lemmy_utils::ApiError;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
use url::Url;
pub mod aggregates;
pub mod source;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
pub trait Crud<Form, IdType> {
fn create(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn read(conn: &PgConnection, id: IdType) -> Result<Self, Error>
where
Self: Sized;
fn update(conn: &PgConnection, id: IdType, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn delete(_conn: &PgConnection, _id: IdType) -> Result<usize, Error>
where
Self: Sized,
{
unimplemented!()
}
}
pub trait Followable<Form> {
fn follow(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn follow_accepted(
conn: &PgConnection,
community_id: CommunityId,
person_id: PersonId,
) -> Result<Self, Error>
where
Self: Sized;
fn unfollow(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
fn has_local_followers(conn: &PgConnection, community_id: CommunityId) -> Result<bool, Error>;
}
pub trait Joinable<Form> {
fn join(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn leave(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Likeable<Form, IdType> {
fn like(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn remove(conn: &PgConnection, person_id: PersonId, item_id: IdType) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Bannable<Form> {
fn ban(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn unban(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Saveable<Form> {
fn save(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn unsave(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Readable<Form> {
fn mark_as_read(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn mark_as_unread(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Reportable<Form> {
fn report(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn resolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
where
Self: Sized;
fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
where
Self: Sized;
}
pub trait ApubObject<Form> {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error>
where
Self: Sized;
fn upsert(conn: &PgConnection, user_form: &Form) -> Result<Self, Error>
where
Self: Sized;
}
pub trait MaybeOptional<T> {
fn get_optional(self) -> Option<T>;
}
impl<T> MaybeOptional<T> for T {
fn get_optional(self) -> Option<T> {
Some(self)
}
}
impl<T> MaybeOptional<T> for Option<T> {
fn get_optional(self) -> Option<T> {
self
}
}
pub trait ToSafe {
type SafeColumns;
fn safe_columns_tuple() -> Self::SafeColumns;
}
pub trait ToSafeSettings {
type SafeSettingsColumns;
fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns;
}
pub trait ViewToVec {
type DbTuple;
fn from_tuple_to_vec(tuple: Vec<Self::DbTuple>) -> Vec<Self>
where
Self: Sized;
}
pub fn get_database_url_from_env() -> Result<String, VarError> {
env::var("LEMMY_DATABASE_URL")
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
pub enum SortType {
Active,
Hot,
New,
TopDay,
TopWeek,
TopMonth,
TopYear,
TopAll,
MostComments,
NewComments,
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone)]
pub enum ListingType {
All,
Local,
Subscribed,
Community,
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
pub enum SearchType {
All,
Comments,
Posts,
Communities,
Users,
Url,
}
pub fn fuzzy_search(q: &str) -> String {
let replaced = q.replace(" ", "%");
format!("%{}%", replaced)
}
pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
let page = page.unwrap_or(1);
let limit = limit.unwrap_or(10);
let offset = limit * (page - 1);
(limit, offset)
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
match opt {
// An empty string is an erase
Some(unwrapped) => {
if !unwrapped.eq("") {
Some(Some(unwrapped.to_owned()))
} else {
Some(None)
}
}
None => None,
}
}
pub fn diesel_option_overwrite_to_url(
opt: &Option<String>,
) -> Result<Option<Option<DbUrl>>, ApiError> {
match opt.as_ref().map(|s| s.as_str()) {
// An empty string is an erase
Some("") => Ok(Some(None)),
Some(str_url) => match Url::parse(str_url) {
Ok(url) => Ok(Some(Some(url.into()))),
Err(_) => Err(ApiError::err("invalid_url")),
},
None => Ok(None),
}
}
embed_migrations!();
pub fn establish_unpooled_connection() -> PgConnection {
let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(e) => panic!(
"Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
e
),
};
let conn =
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
embedded_migrations::run(&conn).expect("load migrations");
conn
}
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex");
}
pub mod functions {
use diesel::sql_types::*;
sql_function! {
fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
}
}
#[cfg(test)]
mod tests {
use super::{fuzzy_search, *};
use crate::is_email_regex;
#[test]
fn test_fuzzy_search() {
let test = "This is a fuzzy search";
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_diesel_option_overwrite() {
assert_eq!(diesel_option_overwrite(&None), None);
assert_eq!(diesel_option_overwrite(&Some("".to_string())), Some(None));
assert_eq!(
diesel_option_overwrite(&Some("test".to_string())),
Some(Some("test".to_string()))
);
}
#[test]
fn test_diesel_option_overwrite_to_url() {
assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None)));
assert!(matches!(
diesel_option_overwrite_to_url(&Some("".to_string())),
Ok(Some(None))
));
assert!(matches!(
diesel_option_overwrite_to_url(&Some("invalid_url".to_string())),
Err(_)
));
let example_url = "https://example.com";
assert!(matches!(
diesel_option_overwrite_to_url(&Some(example_url.to_string())),
Ok(Some(Some(url))) if url == Url::parse(&example_url).unwrap().into()
));
}
}

View file

@ -1,60 +0,0 @@
use crate::Reportable;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
source::comment_report::{CommentReport, CommentReportForm},
PersonId,
};
impl Reportable<CommentReportForm> for CommentReport {
/// creates a comment report and returns it
///
/// * `conn` - the postgres connection
/// * `comment_report_form` - the filled CommentReportForm to insert
fn report(conn: &PgConnection, comment_report_form: &CommentReportForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment_report::dsl::*;
insert_into(comment_report)
.values(comment_report_form)
.get_result::<Self>(conn)
}
/// resolve a comment report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to resolve
/// * `by_resolver_id` - the id of the user resolving the report
fn resolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment_report::dsl::*;
update(comment_report.find(report_id))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
}
/// unresolve a comment report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to unresolve
/// * `by_resolver_id` - the id of the user unresolving the report
fn unresolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment_report::dsl::*;
update(comment_report.find(report_id))
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
}
}

View file

@ -1,493 +0,0 @@
use crate::{ApubObject, Bannable, Crud, Followable, Joinable};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
source::community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityForm,
CommunityModerator,
CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
},
CommunityId,
DbUrl,
PersonId,
};
mod safe_type {
use crate::{source::community::Community, ToSafe};
use lemmy_db_schema::schema::community::*;
type Columns = (
id,
name,
title,
description,
creator_id,
removed,
published,
updated,
deleted,
nsfw,
actor_id,
local,
icon,
banner,
);
impl ToSafe for Community {
type SafeColumns = Columns;
fn safe_columns_tuple() -> Self::SafeColumns {
(
id,
name,
title,
description,
creator_id,
removed,
published,
updated,
deleted,
nsfw,
actor_id,
local,
icon,
banner,
)
}
}
}
impl Crud<CommunityForm, CommunityId> for Community {
fn read(conn: &PgConnection, community_id: CommunityId) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community.find(community_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, community_id: CommunityId) -> Result<usize, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::delete(community.find(community_id)).execute(conn)
}
fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
insert_into(community)
.values(new_community)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
community_id: CommunityId,
new_community: &CommunityForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(new_community)
.get_result::<Self>(conn)
}
}
impl ApubObject<CommunityForm> for Community {
fn read_from_apub_id(conn: &PgConnection, for_actor_id: &DbUrl) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community
.filter(actor_id.eq(for_actor_id))
.first::<Self>(conn)
}
fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
insert_into(community)
.values(community_form)
.on_conflict(actor_id)
.do_update()
.set(community_form)
.get_result::<Self>(conn)
}
}
pub trait Community_ {
fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Community, Error>;
fn update_deleted(
conn: &PgConnection,
community_id: CommunityId,
new_deleted: bool,
) -> Result<Community, Error>;
fn update_removed(
conn: &PgConnection,
community_id: CommunityId,
new_removed: bool,
) -> Result<Community, Error>;
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Community>, Error>;
fn update_creator(
conn: &PgConnection,
community_id: CommunityId,
new_creator_id: PersonId,
) -> Result<Community, Error>;
fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<String>, Error>;
fn read_from_followers_url(
conn: &PgConnection,
followers_url: &DbUrl,
) -> Result<Community, Error>;
}
impl Community_ for Community {
fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community
.filter(local.eq(true))
.filter(name.eq(community_name))
.first::<Self>(conn)
}
fn update_deleted(
conn: &PgConnection,
community_id: CommunityId,
new_deleted: bool,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((deleted.eq(new_deleted), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn update_removed(
conn: &PgConnection,
community_id: CommunityId,
new_removed: bool,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((removed.eq(new_removed), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Community>, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.filter(creator_id.eq(for_creator_id)))
.set((removed.eq(new_removed), updated.eq(naive_now())))
.get_results::<Self>(conn)
}
fn update_creator(
conn: &PgConnection,
community_id: CommunityId,
new_creator_id: PersonId,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<String>, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community.select(actor_id).distinct().load::<String>(conn)
}
fn read_from_followers_url(
conn: &PgConnection,
followers_url_: &DbUrl,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community
.filter(followers_url.eq(followers_url_))
.first::<Self>(conn)
}
}
impl Joinable<CommunityModeratorForm> for CommunityModerator {
fn join(
conn: &PgConnection,
community_moderator_form: &CommunityModeratorForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
insert_into(community_moderator)
.values(community_moderator_form)
.get_result::<Self>(conn)
}
fn leave(
conn: &PgConnection,
community_moderator_form: &CommunityModeratorForm,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
diesel::delete(
community_moderator
.filter(community_id.eq(community_moderator_form.community_id))
.filter(person_id.eq(community_moderator_form.person_id)),
)
.execute(conn)
}
}
pub trait CommunityModerator_ {
fn delete_for_community(
conn: &PgConnection,
for_community_id: CommunityId,
) -> Result<usize, Error>;
fn get_person_moderated_communities(
conn: &PgConnection,
for_person_id: PersonId,
) -> Result<Vec<CommunityId>, Error>;
}
impl CommunityModerator_ for CommunityModerator {
fn delete_for_community(
conn: &PgConnection,
for_community_id: CommunityId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
diesel::delete(community_moderator.filter(community_id.eq(for_community_id))).execute(conn)
}
fn get_person_moderated_communities(
conn: &PgConnection,
for_person_id: PersonId,
) -> Result<Vec<CommunityId>, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
community_moderator
.filter(person_id.eq(for_person_id))
.select(community_id)
.load::<CommunityId>(conn)
}
}
impl Bannable<CommunityPersonBanForm> for CommunityPersonBan {
fn ban(
conn: &PgConnection,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community_person_ban::dsl::*;
insert_into(community_person_ban)
.values(community_person_ban_form)
.get_result::<Self>(conn)
}
fn unban(
conn: &PgConnection,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_person_ban::dsl::*;
diesel::delete(
community_person_ban
.filter(community_id.eq(community_person_ban_form.community_id))
.filter(person_id.eq(community_person_ban_form.person_id)),
)
.execute(conn)
}
}
impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(
conn: &PgConnection,
community_follower_form: &CommunityFollowerForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community_follower::dsl::*;
insert_into(community_follower)
.values(community_follower_form)
.on_conflict((community_id, person_id))
.do_update()
.set(community_follower_form)
.get_result::<Self>(conn)
}
fn follow_accepted(
conn: &PgConnection,
community_id_: CommunityId,
person_id_: PersonId,
) -> Result<Self, Error>
where
Self: Sized,
{
use lemmy_db_schema::schema::community_follower::dsl::*;
diesel::update(
community_follower
.filter(community_id.eq(community_id_))
.filter(person_id.eq(person_id_)),
)
.set(pending.eq(true))
.get_result::<Self>(conn)
}
fn unfollow(
conn: &PgConnection,
community_follower_form: &CommunityFollowerForm,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_follower::dsl::*;
diesel::delete(
community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(person_id.eq(&community_follower_form.person_id)),
)
.execute(conn)
}
// TODO: this function name only makes sense if you call it with a remote community. for a local
// community, it will also return true if only remote followers exist
fn has_local_followers(conn: &PgConnection, community_id_: CommunityId) -> Result<bool, Error> {
use lemmy_db_schema::schema::community_follower::dsl::*;
diesel::select(exists(
community_follower.filter(community_id.eq(community_id_)),
))
.get_result(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, Bannable, Crud, Followable, Joinable};
use lemmy_db_schema::source::{community::*, person::*};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "bobbee".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_community = CommunityForm {
name: "TIL".into(),
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: None,
deleted: None,
updated: None,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let expected_community = Community {
id: inserted_community.id,
creator_id: inserted_person.id,
name: "TIL".into(),
title: "nada".to_owned(),
description: None,
nsfw: false,
removed: false,
deleted: false,
published: inserted_community.published,
updated: None,
actor_id: inserted_community.actor_id.to_owned(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: inserted_community.published,
icon: None,
banner: None,
followers_url: inserted_community.followers_url.to_owned(),
inbox_url: inserted_community.inbox_url.to_owned(),
shared_inbox_url: None,
};
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
pending: false,
};
let inserted_community_follower =
CommunityFollower::follow(&conn, &community_follower_form).unwrap();
let expected_community_follower = CommunityFollower {
id: inserted_community_follower.id,
community_id: inserted_community.id,
person_id: inserted_person.id,
pending: Some(false),
published: inserted_community_follower.published,
};
let community_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
};
let inserted_community_moderator =
CommunityModerator::join(&conn, &community_moderator_form).unwrap();
let expected_community_moderator = CommunityModerator {
id: inserted_community_moderator.id,
community_id: inserted_community.id,
person_id: inserted_person.id,
published: inserted_community_moderator.published,
};
let community_person_ban_form = CommunityPersonBanForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
};
let inserted_community_person_ban =
CommunityPersonBan::ban(&conn, &community_person_ban_form).unwrap();
let expected_community_person_ban = CommunityPersonBan {
id: inserted_community_person_ban.id,
community_id: inserted_community.id,
person_id: inserted_person.id,
published: inserted_community_person_ban.published,
};
let read_community = Community::read(&conn, inserted_community.id).unwrap();
let updated_community =
Community::update(&conn, inserted_community.id, &new_community).unwrap();
let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
let left_community = CommunityModerator::leave(&conn, &community_moderator_form).unwrap();
let unban = CommunityPersonBan::unban(&conn, &community_person_ban_form).unwrap();
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(expected_community, read_community);
assert_eq!(expected_community, inserted_community);
assert_eq!(expected_community, updated_community);
assert_eq!(expected_community_follower, inserted_community_follower);
assert_eq!(expected_community_moderator, inserted_community_moderator);
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
assert_eq!(1, ignored_community);
assert_eq!(1, left_community);
assert_eq!(1, unban);
// assert_eq!(2, loaded_count);
assert_eq!(1, num_deleted);
}
}

View file

@ -1,119 +0,0 @@
use crate::Crud;
use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
schema::local_user::dsl::*,
source::local_user::{LocalUser, LocalUserForm},
LocalUserId,
PersonId,
};
mod safe_settings_type {
use crate::ToSafeSettings;
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
type Columns = (
id,
person_id,
email,
admin,
show_nsfw,
theme,
default_sort_type,
default_listing_type,
lang,
show_avatars,
send_notifications_to_email,
matrix_user_id,
validator_time,
);
impl ToSafeSettings for LocalUser {
type SafeSettingsColumns = Columns;
/// Includes everything but the hashed password
fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns {
(
id,
person_id,
email,
admin,
show_nsfw,
theme,
default_sort_type,
default_listing_type,
lang,
show_avatars,
send_notifications_to_email,
matrix_user_id,
validator_time,
)
}
}
}
pub trait LocalUser_ {
fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<LocalUser, Error>;
fn update_password(
conn: &PgConnection,
local_user_id: LocalUserId,
new_password: &str,
) -> Result<LocalUser, Error>;
fn add_admin(conn: &PgConnection, person_id: PersonId, added: bool) -> Result<LocalUser, Error>;
}
impl LocalUser_ for LocalUser {
fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
Self::create(&conn, &edited_user)
}
fn update_password(
conn: &PgConnection,
local_user_id: LocalUserId,
new_password: &str,
) -> Result<Self, Error> {
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
diesel::update(local_user.find(local_user_id))
.set((
password_encrypted.eq(password_hash),
validator_time.eq(naive_now()),
))
.get_result::<Self>(conn)
}
fn add_admin(conn: &PgConnection, for_person_id: PersonId, added: bool) -> Result<Self, Error> {
diesel::update(local_user.filter(person_id.eq(for_person_id)))
.set(admin.eq(added))
.get_result::<Self>(conn)
}
}
impl Crud<LocalUserForm, LocalUserId> for LocalUser {
fn read(conn: &PgConnection, local_user_id: LocalUserId) -> Result<Self, Error> {
local_user.find(local_user_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, local_user_id: LocalUserId) -> Result<usize, Error> {
diesel::delete(local_user.find(local_user_id)).execute(conn)
}
fn create(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
insert_into(local_user)
.values(form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
local_user_id: LocalUserId,
form: &LocalUserForm,
) -> Result<Self, Error> {
diesel::update(local_user.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
}
}

View file

@ -1,13 +0,0 @@
pub mod activity;
pub mod comment;
pub mod comment_report;
pub mod community;
pub mod local_user;
pub mod moderator;
pub mod password_reset_request;
pub mod person;
pub mod person_mention;
pub mod post;
pub mod post_report;
pub mod private_message;
pub mod site;

View file

@ -1,290 +0,0 @@
use crate::{ApubObject, Crud};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
schema::person::dsl::*,
source::person::{Person, PersonForm},
DbUrl,
PersonId,
};
mod safe_type {
use crate::ToSafe;
use lemmy_db_schema::{schema::person::columns::*, source::person::Person};
type Columns = (
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
);
impl ToSafe for Person {
type SafeColumns = Columns;
fn safe_columns_tuple() -> Self::SafeColumns {
(
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
)
}
}
}
mod safe_type_alias_1 {
use crate::ToSafe;
use lemmy_db_schema::{schema::person_alias_1::columns::*, source::person::PersonAlias1};
type Columns = (
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
);
impl ToSafe for PersonAlias1 {
type SafeColumns = Columns;
fn safe_columns_tuple() -> Self::SafeColumns {
(
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
)
}
}
}
mod safe_type_alias_2 {
use crate::ToSafe;
use lemmy_db_schema::{schema::person_alias_2::columns::*, source::person::PersonAlias2};
type Columns = (
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
);
impl ToSafe for PersonAlias2 {
type SafeColumns = Columns;
fn safe_columns_tuple() -> Self::SafeColumns {
(
id,
name,
preferred_username,
avatar,
banned,
published,
updated,
actor_id,
bio,
local,
banner,
deleted,
inbox_url,
shared_inbox_url,
)
}
}
}
impl Crud<PersonForm, PersonId> for Person {
fn read(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
person
.filter(deleted.eq(false))
.find(person_id)
.first::<Self>(conn)
}
fn delete(conn: &PgConnection, person_id: PersonId) -> Result<usize, Error> {
diesel::delete(person.find(person_id)).execute(conn)
}
fn create(conn: &PgConnection, form: &PersonForm) -> Result<Self, Error> {
insert_into(person).values(form).get_result::<Self>(conn)
}
fn update(conn: &PgConnection, person_id: PersonId, form: &PersonForm) -> Result<Self, Error> {
diesel::update(person.find(person_id))
.set(form)
.get_result::<Self>(conn)
}
}
impl ApubObject<PersonForm> for Person {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error> {
use lemmy_db_schema::schema::person::dsl::*;
person
.filter(deleted.eq(false))
.filter(actor_id.eq(object_id))
.first::<Self>(conn)
}
fn upsert(conn: &PgConnection, person_form: &PersonForm) -> Result<Person, Error> {
insert_into(person)
.values(person_form)
.on_conflict(actor_id)
.do_update()
.set(person_form)
.get_result::<Self>(conn)
}
}
pub trait Person_ {
fn ban_person(conn: &PgConnection, person_id: PersonId, ban: bool) -> Result<Person, Error>;
fn find_by_name(conn: &PgConnection, name: &str) -> Result<Person, Error>;
fn mark_as_updated(conn: &PgConnection, person_id: PersonId) -> Result<Person, Error>;
fn delete_account(conn: &PgConnection, person_id: PersonId) -> Result<Person, Error>;
}
impl Person_ for Person {
fn ban_person(conn: &PgConnection, person_id: PersonId, ban: bool) -> Result<Self, Error> {
diesel::update(person.find(person_id))
.set(banned.eq(ban))
.get_result::<Self>(conn)
}
fn find_by_name(conn: &PgConnection, from_name: &str) -> Result<Person, Error> {
person
.filter(deleted.eq(false))
.filter(local.eq(true))
.filter(name.ilike(from_name))
.first::<Person>(conn)
}
fn mark_as_updated(conn: &PgConnection, person_id: PersonId) -> Result<Person, Error> {
diesel::update(person.find(person_id))
.set((last_refreshed_at.eq(naive_now()),))
.get_result::<Self>(conn)
}
fn delete_account(conn: &PgConnection, person_id: PersonId) -> Result<Person, Error> {
use lemmy_db_schema::schema::local_user;
// Set the local user info to none
diesel::update(local_user::table.filter(local_user::person_id.eq(person_id)))
.set((
local_user::email.eq::<Option<String>>(None),
local_user::matrix_user_id.eq::<Option<String>>(None),
))
.execute(conn)?;
diesel::update(person.find(person_id))
.set((
preferred_username.eq::<Option<String>>(None),
bio.eq::<Option<String>>(None),
deleted.eq(true),
updated.eq(naive_now()),
))
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, source::person::*};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "holly".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let expected_person = Person {
id: inserted_person.id,
name: "holly".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: false,
deleted: false,
published: inserted_person.published,
updated: None,
actor_id: inserted_person.actor_id.to_owned(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: inserted_person.published,
inbox_url: inserted_person.inbox_url.to_owned(),
shared_inbox_url: None,
};
let read_person = Person::read(&conn, inserted_person.id).unwrap();
let updated_person = Person::update(&conn, inserted_person.id, &new_person).unwrap();
let num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(expected_person, read_person);
assert_eq!(expected_person, inserted_person);
assert_eq!(expected_person, updated_person);
assert_eq!(1, num_deleted);
}
}

View file

@ -1,225 +0,0 @@
use crate::Crud;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{source::person_mention::*, PersonId, PersonMentionId};
impl Crud<PersonMentionForm, PersonMentionId> for PersonMention {
fn read(conn: &PgConnection, person_mention_id: PersonMentionId) -> Result<Self, Error> {
use lemmy_db_schema::schema::person_mention::dsl::*;
person_mention.find(person_mention_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, person_mention_form: &PersonMentionForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::person_mention::dsl::*;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesnt return the existing row here
insert_into(person_mention)
.values(person_mention_form)
.on_conflict((recipient_id, comment_id))
.do_update()
.set(person_mention_form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
person_mention_id: PersonMentionId,
person_mention_form: &PersonMentionForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::person_mention::dsl::*;
diesel::update(person_mention.find(person_mention_id))
.set(person_mention_form)
.get_result::<Self>(conn)
}
}
pub trait PersonMention_ {
fn update_read(
conn: &PgConnection,
person_mention_id: PersonMentionId,
new_read: bool,
) -> Result<PersonMention, Error>;
fn mark_all_as_read(
conn: &PgConnection,
for_recipient_id: PersonId,
) -> Result<Vec<PersonMention>, Error>;
}
impl PersonMention_ for PersonMention {
fn update_read(
conn: &PgConnection,
person_mention_id: PersonMentionId,
new_read: bool,
) -> Result<PersonMention, Error> {
use lemmy_db_schema::schema::person_mention::dsl::*;
diesel::update(person_mention.find(person_mention_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
fn mark_all_as_read(
conn: &PgConnection,
for_recipient_id: PersonId,
) -> Result<Vec<PersonMention>, Error> {
use lemmy_db_schema::schema::person_mention::dsl::*;
diesel::update(
person_mention
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, Crud};
use lemmy_db_schema::source::{
comment::*,
community::{Community, CommunityForm},
person::*,
person_mention::*,
post::*,
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "terrylake".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let recipient_form = PersonForm {
name: "terrylakes recipient".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_recipient = Person::create(&conn, &recipient_form).unwrap();
let new_community = CommunityForm {
name: "test community lake".to_string(),
title: "nada".to_owned(),
description: None,
creator_id: inserted_person.id,
removed: None,
deleted: None,
updated: None,
nsfw: false,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
creator_id: inserted_person.id,
url: None,
body: None,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
updated: None,
nsfw: false,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let person_mention_form = PersonMentionForm {
recipient_id: inserted_recipient.id,
comment_id: inserted_comment.id,
read: None,
};
let inserted_mention = PersonMention::create(&conn, &person_mention_form).unwrap();
let expected_mention = PersonMention {
id: inserted_mention.id,
recipient_id: inserted_mention.recipient_id,
comment_id: inserted_mention.comment_id,
read: false,
published: inserted_mention.published,
};
let read_mention = PersonMention::read(&conn, inserted_mention.id).unwrap();
let updated_mention =
PersonMention::update(&conn, inserted_mention.id, &person_mention_form).unwrap();
Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
Person::delete(&conn, inserted_recipient.id).unwrap();
assert_eq!(expected_mention, read_mention);
assert_eq!(expected_mention, inserted_mention);
assert_eq!(expected_mention, updated_mention);
}
}

View file

@ -1,56 +0,0 @@
use crate::Reportable;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{naive_now, source::post_report::*, PersonId};
impl Reportable<PostReportForm> for PostReport {
/// creates a post report and returns it
///
/// * `conn` - the postgres connection
/// * `post_report_form` - the filled CommentReportForm to insert
fn report(conn: &PgConnection, post_report_form: &PostReportForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::post_report::dsl::*;
insert_into(post_report)
.values(post_report_form)
.get_result::<Self>(conn)
}
/// resolve a post report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to resolve
/// * `by_resolver_id` - the id of the user resolving the report
fn resolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::post_report::dsl::*;
update(post_report.find(report_id))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
}
/// resolve a post report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to unresolve
/// * `by_resolver_id` - the id of the user unresolving the report
fn unresolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::post_report::dsl::*;
update(post_report.find(report_id))
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
}
}

View file

@ -1,45 +0,0 @@
use crate::Crud;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{naive_now, source::site::*, PersonId};
impl Crud<SiteForm, i32> for Site {
fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
use lemmy_db_schema::schema::site::dsl::*;
site.first::<Self>(conn)
}
fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::site::dsl::*;
insert_into(site).values(new_site).get_result::<Self>(conn)
}
fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::site::dsl::*;
diesel::update(site.find(site_id))
.set(new_site)
.get_result::<Self>(conn)
}
fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
use lemmy_db_schema::schema::site::dsl::*;
diesel::delete(site.find(site_id)).execute(conn)
}
}
pub trait Site_ {
fn transfer(conn: &PgConnection, new_creator_id: PersonId) -> Result<Site, Error>;
fn read_simple(conn: &PgConnection) -> Result<Site, Error>;
}
impl Site_ for Site {
fn transfer(conn: &PgConnection, new_creator_id: PersonId) -> Result<Site, Error> {
use lemmy_db_schema::schema::site::dsl::*;
diesel::update(site.find(1))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn read_simple(conn: &PgConnection) -> Result<Self, Error> {
use lemmy_db_schema::schema::site::dsl::*;
site.first::<Self>(conn)
}
}

View file

@ -1,16 +0,0 @@
[package]
name = "lemmy_db_schema"
version = "0.1.0"
edition = "2018"
[lib]
doctest = false
[dependencies]
diesel = { version = "1.4.5", features = ["postgres","chrono","r2d2","serde_json"] }
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.123", features = ["derive"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
log = "0.4.14"
url = { version = "2.2.1", features = ["serde"] }
diesel-derive-newtype = "0.1"

View file

@ -1,115 +0,0 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_derive_newtype;
use chrono::NaiveDateTime;
use diesel::{
backend::Backend,
deserialize::FromSql,
serialize::{Output, ToSql},
sql_types::Text,
};
use serde::{Deserialize, Serialize};
use std::{
fmt,
fmt::{Display, Formatter},
io::Write,
};
use url::Url;
pub mod schema;
pub mod source;
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct PostId(pub i32);
impl fmt::Display for PostId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct PersonId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct CommentId(pub i32);
impl fmt::Display for CommentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct CommunityId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct LocalUserId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct PrivateMessageId(i32);
impl fmt::Display for PrivateMessageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
pub struct PersonMentionId(i32);
#[repr(transparent)]
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, AsExpression, FromSqlRow)]
#[sql_type = "Text"]
pub struct DbUrl(Url);
impl<DB: Backend> ToSql<Text, DB> for DbUrl
where
String: ToSql<Text, DB>,
{
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> diesel::serialize::Result {
self.0.to_string().to_sql(out)
}
}
impl<DB: Backend> FromSql<Text, DB> for DbUrl
where
String: FromSql<Text, DB>,
{
fn from_sql(bytes: Option<&DB::RawValue>) -> diesel::deserialize::Result<Self> {
let str = String::from_sql(bytes)?;
Ok(DbUrl(Url::parse(&str)?))
}
}
impl DbUrl {
pub fn into_inner(self) -> Url {
self.0
}
}
impl Display for DbUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.to_owned().into_inner().fmt(f)
}
}
impl From<DbUrl> for Url {
fn from(url: DbUrl) -> Self {
url.0
}
}
impl From<Url> for DbUrl {
fn from(url: Url) -> Self {
DbUrl(url)
}
}
// TODO: can probably move this back to lemmy_db_queries
pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}

View file

@ -1,25 +0,0 @@
use crate::{schema::activity, DbUrl};
use serde_json::Value;
use std::fmt::Debug;
#[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "activity"]
pub struct Activity {
pub id: i32,
pub data: Value,
pub local: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: Option<DbUrl>,
pub sensitive: Option<bool>,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "activity"]
pub struct ActivityForm {
pub data: Value,
pub local: bool,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: DbUrl,
pub sensitive: bool,
}

View file

@ -1,106 +0,0 @@
use crate::{
schema::{comment, comment_alias_1, comment_like, comment_saved},
source::post::Post,
CommentId,
DbUrl,
PersonId,
PostId,
};
use serde::Serialize;
// WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL
// UNION ALL
// SELECT m.* FROM comment AS m JOIN MyTree AS t ON m.parent_id = t.id
// )
// SELECT * FROM MyTree;
#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)]
#[belongs_to(Post)]
#[table_name = "comment"]
pub struct Comment {
pub id: CommentId,
pub creator_id: PersonId,
pub post_id: PostId,
pub parent_id: Option<CommentId>,
pub content: String,
pub removed: bool,
pub read: bool, // Whether the recipient has read the comment or not
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: DbUrl,
pub local: bool,
}
#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)]
#[belongs_to(Post)]
#[table_name = "comment_alias_1"]
pub struct CommentAlias1 {
pub id: CommentId,
pub creator_id: PersonId,
pub post_id: PostId,
pub parent_id: Option<CommentId>,
pub content: String,
pub removed: bool,
pub read: bool, // Whether the recipient has read the comment or not
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: DbUrl,
pub local: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "comment"]
pub struct CommentForm {
pub creator_id: PersonId,
pub post_id: PostId,
pub parent_id: Option<CommentId>,
pub content: String,
pub removed: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub ap_id: Option<DbUrl>,
pub local: bool,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
#[belongs_to(Comment)]
#[table_name = "comment_like"]
pub struct CommentLike {
pub id: i32,
pub person_id: PersonId,
pub comment_id: CommentId,
pub post_id: PostId, // TODO this is redundant
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "comment_like"]
pub struct CommentLikeForm {
pub person_id: PersonId,
pub comment_id: CommentId,
pub post_id: PostId, // TODO this is redundant
pub score: i16,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Comment)]
#[table_name = "comment_saved"]
pub struct CommentSaved {
pub id: i32,
pub comment_id: CommentId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "comment_saved"]
pub struct CommentSavedForm {
pub comment_id: CommentId,
pub person_id: PersonId,
}

View file

@ -1,28 +0,0 @@
use crate::{schema::comment_report, source::comment::Comment, CommentId, PersonId};
use serde::{Deserialize, Serialize};
#[derive(
Identifiable, Queryable, Associations, PartialEq, Serialize, Deserialize, Debug, Clone,
)]
#[belongs_to(Comment)]
#[table_name = "comment_report"]
pub struct CommentReport {
pub id: i32,
pub creator_id: PersonId,
pub comment_id: CommentId,
pub original_comment_text: String,
pub reason: String,
pub resolved: bool,
pub resolver_id: Option<PersonId>,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "comment_report"]
pub struct CommentReportForm {
pub creator_id: PersonId,
pub comment_id: CommentId,
pub original_comment_text: String,
pub reason: String,
}

View file

@ -1,129 +0,0 @@
use crate::{
schema::{community, community_follower, community_moderator, community_person_ban},
CommunityId,
DbUrl,
PersonId,
};
use serde::Serialize;
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "community"]
pub struct Community {
pub id: CommunityId,
pub name: String,
pub title: String,
pub description: Option<String>,
pub creator_id: PersonId,
pub removed: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub actor_id: DbUrl,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
pub icon: Option<DbUrl>,
pub banner: Option<DbUrl>,
pub followers_url: DbUrl,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
/// A safe representation of community, without the sensitive info
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "community"]
pub struct CommunitySafe {
pub id: CommunityId,
pub name: String,
pub title: String,
pub description: Option<String>,
pub creator_id: PersonId,
pub removed: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub actor_id: DbUrl,
pub local: bool,
pub icon: Option<DbUrl>,
pub banner: Option<DbUrl>,
}
#[derive(Insertable, AsChangeset, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
pub name: String,
pub title: String,
pub description: Option<String>,
pub creator_id: PersonId,
pub removed: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub nsfw: bool,
pub actor_id: Option<DbUrl>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
pub icon: Option<Option<DbUrl>>,
pub banner: Option<Option<DbUrl>>,
pub followers_url: Option<DbUrl>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<Option<DbUrl>>,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_moderator"]
pub struct CommunityModerator {
pub id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "community_moderator"]
pub struct CommunityModeratorForm {
pub community_id: CommunityId,
pub person_id: PersonId,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_person_ban"]
pub struct CommunityPersonBan {
pub id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "community_person_ban"]
pub struct CommunityPersonBanForm {
pub community_id: CommunityId,
pub person_id: PersonId,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Community)]
#[table_name = "community_follower"]
pub struct CommunityFollower {
pub id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
pub pending: Option<bool>,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "community_follower"]
pub struct CommunityFollowerForm {
pub community_id: CommunityId,
pub person_id: PersonId,
pub pending: bool,
}

View file

@ -1,58 +0,0 @@
use crate::{schema::local_user, LocalUserId, PersonId};
use serde::Serialize;
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "local_user"]
pub struct LocalUser {
pub id: LocalUserId,
pub person_id: PersonId,
pub password_encrypted: String,
pub email: Option<String>,
pub admin: bool,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>,
pub validator_time: chrono::NaiveDateTime,
}
// TODO redo these, check table defaults
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "local_user"]
pub struct LocalUserForm {
pub person_id: PersonId,
pub password_encrypted: String,
pub email: Option<Option<String>>,
pub admin: Option<bool>,
pub show_nsfw: Option<bool>,
pub theme: Option<String>,
pub default_sort_type: Option<i16>,
pub default_listing_type: Option<i16>,
pub lang: Option<String>,
pub show_avatars: Option<bool>,
pub send_notifications_to_email: Option<bool>,
pub matrix_user_id: Option<Option<String>>,
}
/// A local user view that removes password encrypted
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "local_user"]
pub struct LocalUserSettings {
pub id: LocalUserId,
pub person_id: PersonId,
pub email: Option<String>,
pub admin: bool,
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>,
pub validator_time: chrono::NaiveDateTime,
}

View file

@ -1,13 +0,0 @@
pub mod activity;
pub mod comment;
pub mod comment_report;
pub mod community;
pub mod local_user;
pub mod moderator;
pub mod password_reset_request;
pub mod person;
pub mod person_mention;
pub mod post;
pub mod post_report;
pub mod private_message;
pub mod site;

View file

@ -1,200 +0,0 @@
use crate::{
schema::{
mod_add,
mod_add_community,
mod_ban,
mod_ban_from_community,
mod_lock_post,
mod_remove_comment,
mod_remove_community,
mod_remove_post,
mod_sticky_post,
},
CommentId,
CommunityId,
PersonId,
PostId,
};
use serde::Serialize;
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_remove_post"]
pub struct ModRemovePost {
pub id: i32,
pub mod_person_id: PersonId,
pub post_id: PostId,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_remove_post"]
pub struct ModRemovePostForm {
pub mod_person_id: PersonId,
pub post_id: PostId,
pub reason: Option<String>,
pub removed: Option<bool>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_lock_post"]
pub struct ModLockPost {
pub id: i32,
pub mod_person_id: PersonId,
pub post_id: PostId,
pub locked: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_lock_post"]
pub struct ModLockPostForm {
pub mod_person_id: PersonId,
pub post_id: PostId,
pub locked: Option<bool>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_sticky_post"]
pub struct ModStickyPost {
pub id: i32,
pub mod_person_id: PersonId,
pub post_id: PostId,
pub stickied: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_sticky_post"]
pub struct ModStickyPostForm {
pub mod_person_id: PersonId,
pub post_id: PostId,
pub stickied: Option<bool>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_remove_comment"]
pub struct ModRemoveComment {
pub id: i32,
pub mod_person_id: PersonId,
pub comment_id: CommentId,
pub reason: Option<String>,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_remove_comment"]
pub struct ModRemoveCommentForm {
pub mod_person_id: PersonId,
pub comment_id: CommentId,
pub reason: Option<String>,
pub removed: Option<bool>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_remove_community"]
pub struct ModRemoveCommunity {
pub id: i32,
pub mod_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
pub removed: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_remove_community"]
pub struct ModRemoveCommunityForm {
pub mod_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
pub removed: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_ban_from_community"]
pub struct ModBanFromCommunity {
pub id: i32,
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_ban_from_community"]
pub struct ModBanFromCommunityForm {
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub community_id: CommunityId,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_ban"]
pub struct ModBan {
pub id: i32,
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_ban"]
pub struct ModBanForm {
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub reason: Option<String>,
pub banned: Option<bool>,
pub expires: Option<chrono::NaiveDateTime>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_add_community"]
pub struct ModAddCommunity {
pub id: i32,
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub community_id: CommunityId,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_add_community"]
pub struct ModAddCommunityForm {
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub community_id: CommunityId,
pub removed: Option<bool>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "mod_add"]
pub struct ModAdd {
pub id: i32,
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub removed: Option<bool>,
pub when_: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "mod_add"]
pub struct ModAddForm {
pub mod_person_id: PersonId,
pub other_person_id: PersonId,
pub removed: Option<bool>,
}

View file

@ -1,17 +0,0 @@
use crate::{schema::password_reset_request, LocalUserId};
#[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "password_reset_request"]
pub struct PasswordResetRequest {
pub id: i32,
pub token_encrypted: String,
pub published: chrono::NaiveDateTime,
pub local_user_id: LocalUserId,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "password_reset_request"]
pub struct PasswordResetRequestForm {
pub local_user_id: LocalUserId,
pub token_encrypted: String,
}

View file

@ -1,151 +0,0 @@
use crate::{
schema::{person, person_alias_1, person_alias_2},
DbUrl,
PersonId,
};
use serde::Serialize;
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person"]
pub struct Person {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
/// A safe representation of person, without the sensitive info
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person"]
pub struct PersonSafe {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person_alias_1"]
pub struct PersonAlias1 {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person_alias_1"]
pub struct PersonSafeAlias1 {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person_alias_2"]
pub struct PersonAlias2 {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "person_alias_1"]
pub struct PersonSafeAlias2 {
pub id: PersonId,
pub name: String,
pub preferred_username: Option<String>,
pub avatar: Option<DbUrl>,
pub banned: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: DbUrl,
pub bio: Option<String>,
pub local: bool,
pub banner: Option<DbUrl>,
pub deleted: bool,
pub inbox_url: DbUrl,
pub shared_inbox_url: Option<DbUrl>,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "person"]
pub struct PersonForm {
pub name: String,
pub preferred_username: Option<Option<String>>,
pub avatar: Option<Option<DbUrl>>,
pub banned: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub actor_id: Option<DbUrl>,
pub bio: Option<Option<String>>,
pub local: Option<bool>,
pub private_key: Option<Option<String>>,
pub public_key: Option<Option<String>>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
pub banner: Option<Option<DbUrl>>,
pub deleted: Option<bool>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<Option<DbUrl>>,
}

View file

@ -1,27 +0,0 @@
use crate::{
schema::person_mention,
source::comment::Comment,
CommentId,
PersonId,
PersonMentionId,
};
use serde::Serialize;
#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)]
#[belongs_to(Comment)]
#[table_name = "person_mention"]
pub struct PersonMention {
pub id: PersonMentionId,
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: bool,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "person_mention"]
pub struct PersonMentionForm {
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: Option<bool>,
}

View file

@ -1,108 +0,0 @@
use crate::{
schema::{post, post_like, post_read, post_saved},
CommunityId,
DbUrl,
PersonId,
PostId,
};
use serde::Serialize;
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "post"]
pub struct Post {
pub id: PostId,
pub name: String,
pub url: Option<DbUrl>,
pub body: Option<String>,
pub creator_id: PersonId,
pub community_id: CommunityId,
pub removed: bool,
pub locked: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub stickied: bool,
pub embed_title: Option<String>,
pub embed_description: Option<String>,
pub embed_html: Option<String>,
pub thumbnail_url: Option<DbUrl>,
pub ap_id: DbUrl,
pub local: bool,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "post"]
pub struct PostForm {
pub name: String,
pub url: Option<DbUrl>,
pub body: Option<String>,
pub creator_id: PersonId,
pub community_id: CommunityId,
pub removed: Option<bool>,
pub locked: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub nsfw: bool,
pub stickied: Option<bool>,
pub embed_title: Option<String>,
pub embed_description: Option<String>,
pub embed_html: Option<String>,
pub thumbnail_url: Option<DbUrl>,
pub ap_id: Option<DbUrl>,
pub local: bool,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_like"]
pub struct PostLike {
pub id: i32,
pub post_id: PostId,
pub person_id: PersonId,
pub score: i16,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "post_like"]
pub struct PostLikeForm {
pub post_id: PostId,
pub person_id: PersonId,
pub score: i16,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_saved"]
pub struct PostSaved {
pub id: i32,
pub post_id: PostId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "post_saved"]
pub struct PostSavedForm {
pub post_id: PostId,
pub person_id: PersonId,
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
#[belongs_to(Post)]
#[table_name = "post_read"]
pub struct PostRead {
pub id: i32,
pub post_id: PostId,
pub person_id: PersonId,
pub published: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "post_read"]
pub struct PostReadForm {
pub post_id: PostId,
pub person_id: PersonId,
}

View file

@ -1,32 +0,0 @@
use crate::{schema::post_report, source::post::Post, DbUrl, PersonId, PostId};
use serde::{Deserialize, Serialize};
#[derive(
Identifiable, Queryable, Associations, PartialEq, Serialize, Deserialize, Debug, Clone,
)]
#[belongs_to(Post)]
#[table_name = "post_report"]
pub struct PostReport {
pub id: i32,
pub creator_id: PersonId,
pub post_id: PostId,
pub original_post_name: String,
pub original_post_url: Option<DbUrl>,
pub original_post_body: Option<String>,
pub reason: String,
pub resolved: bool,
pub resolver_id: Option<PersonId>,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
}
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "post_report"]
pub struct PostReportForm {
pub creator_id: PersonId,
pub post_id: PostId,
pub original_post_name: String,
pub original_post_url: Option<DbUrl>,
pub original_post_body: Option<String>,
pub reason: String,
}

View file

@ -1,31 +0,0 @@
use crate::{schema::private_message, DbUrl, PersonId, PrivateMessageId};
use serde::Serialize;
#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "private_message"]
pub struct PrivateMessage {
pub id: PrivateMessageId,
pub creator_id: PersonId,
pub recipient_id: PersonId,
pub content: String,
pub deleted: bool,
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: DbUrl,
pub local: bool,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "private_message"]
pub struct PrivateMessageForm {
pub creator_id: PersonId,
pub recipient_id: PersonId,
pub content: String,
pub deleted: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: Option<DbUrl>,
pub local: bool,
}

View file

@ -1,33 +0,0 @@
use crate::{schema::site, DbUrl, PersonId};
use serde::Serialize;
#[derive(Queryable, Identifiable, PartialEq, Debug, Clone, Serialize)]
#[table_name = "site"]
pub struct Site {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: PersonId,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub icon: Option<DbUrl>,
pub banner: Option<DbUrl>,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "site"]
pub struct SiteForm {
pub name: String,
pub description: Option<String>,
pub creator_id: PersonId,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
// when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column.
pub icon: Option<Option<DbUrl>>,
pub banner: Option<Option<DbUrl>>,
}

View file

@ -1,18 +0,0 @@
[package]
name = "lemmy_db_views"
version = "0.1.0"
edition = "2018"
[lib]
doctest = false
[dependencies]
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
diesel = { version = "1.4.5", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.123", features = ["derive"] }
log = "0.4.14"
url = "2.2.1"
[dev-dependencies]
serial_test = "0.5.1"

View file

@ -1,194 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{limit_and_offset, MaybeOptional, ToSafe, ViewToVec};
use lemmy_db_schema::{
schema::{comment, comment_report, community, person, person_alias_1, person_alias_2, post},
source::{
comment::Comment,
comment_report::CommentReport,
community::{Community, CommunitySafe},
person::{Person, PersonAlias1, PersonAlias2, PersonSafe, PersonSafeAlias1, PersonSafeAlias2},
post::Post,
},
CommunityId,
};
use serde::Serialize;
#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct CommentReportView {
pub comment_report: CommentReport,
pub comment: Comment,
pub post: Post,
pub community: CommunitySafe,
pub creator: PersonSafe,
pub comment_creator: PersonSafeAlias1,
pub resolver: Option<PersonSafeAlias2>,
}
type CommentReportViewTuple = (
CommentReport,
Comment,
Post,
CommunitySafe,
PersonSafe,
PersonSafeAlias1,
Option<PersonSafeAlias2>,
);
impl CommentReportView {
/// returns the CommentReportView for the provided report_id
///
/// * `report_id` - the report id to obtain
pub fn read(conn: &PgConnection, report_id: i32) -> Result<Self, Error> {
let (comment_report, comment, post, community, creator, comment_creator, resolver) =
comment_report::table
.find(report_id)
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(post::creator_id.eq(person_alias_1::id)))
.left_join(
person_alias_2::table.on(comment_report::resolver_id.eq(person_alias_2::id.nullable())),
)
.select((
comment_report::all_columns,
comment::all_columns,
post::all_columns,
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
PersonAlias2::safe_columns_tuple().nullable(),
))
.first::<CommentReportViewTuple>(conn)?;
Ok(Self {
comment_report,
comment,
post,
community,
creator,
comment_creator,
resolver,
})
}
/// returns the current unresolved post report count for the supplied community ids
///
/// * `community_ids` - a Vec<i32> of community_ids to get a count for
/// TODO this eq_any is a bad way to do this, would be better to join to communitymoderator
/// for a person id
pub fn get_report_count(
conn: &PgConnection,
community_ids: &[CommunityId],
) -> Result<i64, Error> {
use diesel::dsl::*;
comment_report::table
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.filter(
comment_report::resolved
.eq(false)
.and(post::community_id.eq_any(community_ids)),
)
.select(count(comment_report::id))
.first::<i64>(conn)
}
}
pub struct CommentReportQueryBuilder<'a> {
conn: &'a PgConnection,
community_ids: Option<Vec<CommunityId>>, // TODO bad way to do this
page: Option<i64>,
limit: Option<i64>,
resolved: Option<bool>,
}
impl<'a> CommentReportQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self {
CommentReportQueryBuilder {
conn,
community_ids: None,
page: None,
limit: None,
resolved: Some(false),
}
}
pub fn community_ids<T: MaybeOptional<Vec<CommunityId>>>(mut self, community_ids: T) -> Self {
self.community_ids = community_ids.get_optional();
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn resolved<T: MaybeOptional<bool>>(mut self, resolved: T) -> Self {
self.resolved = resolved.get_optional();
self
}
pub fn list(self) -> Result<Vec<CommentReportView>, Error> {
let mut query = comment_report::table
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(comment_report::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(post::creator_id.eq(person_alias_1::id)))
.left_join(
person_alias_2::table.on(comment_report::resolver_id.eq(person_alias_2::id.nullable())),
)
.select((
comment_report::all_columns,
comment::all_columns,
post::all_columns,
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
PersonAlias2::safe_columns_tuple().nullable(),
))
.into_boxed();
if let Some(comm_ids) = self.community_ids {
query = query.filter(post::community_id.eq_any(comm_ids));
}
if let Some(resolved_flag) = self.resolved {
query = query.filter(comment_report::resolved.eq(resolved_flag));
}
let (limit, offset) = limit_and_offset(self.page, self.limit);
let res = query
.order_by(comment_report::published.asc())
.limit(limit)
.offset(offset)
.load::<CommentReportViewTuple>(self.conn)?;
Ok(CommentReportView::from_tuple_to_vec(res))
}
}
impl ViewToVec for CommentReportView {
type DbTuple = CommentReportViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
comment_report: a.0.to_owned(),
comment: a.1.to_owned(),
post: a.2.to_owned(),
community: a.3.to_owned(),
creator: a.4.to_owned(),
comment_creator: a.5.to_owned(),
resolver: a.6.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -1,664 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{
aggregates::comment_aggregates::CommentAggregates,
functions::hot_rank,
fuzzy_search,
limit_and_offset,
ListingType,
MaybeOptional,
SortType,
ToSafe,
ViewToVec,
};
use lemmy_db_schema::{
schema::{
comment,
comment_aggregates,
comment_alias_1,
comment_like,
comment_saved,
community,
community_follower,
community_person_ban,
person,
person_alias_1,
post,
},
source::{
comment::{Comment, CommentAlias1, CommentSaved},
community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1},
post::Post,
},
CommentId,
CommunityId,
PersonId,
PostId,
};
use serde::Serialize;
#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct CommentView {
pub comment: Comment,
pub creator: PersonSafe,
pub recipient: Option<PersonSafeAlias1>, // Left joins to comment and person
pub post: Post,
pub community: CommunitySafe,
pub counts: CommentAggregates,
pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
pub subscribed: bool, // Left join to CommunityFollower
pub saved: bool, // Left join to CommentSaved
pub my_vote: Option<i16>, // Left join to CommentLike
}
type CommentViewTuple = (
Comment,
PersonSafe,
Option<CommentAlias1>,
Option<PersonSafeAlias1>,
Post,
CommunitySafe,
CommentAggregates,
Option<CommunityPersonBan>,
Option<CommunityFollower>,
Option<CommentSaved>,
Option<i16>,
);
impl CommentView {
pub fn read(
conn: &PgConnection,
comment_id: CommentId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
// The left join below will return None in this case
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
let (
comment,
creator,
_parent_comment,
recipient,
post,
community,
counts,
creator_banned_from_community,
subscribed,
saved,
comment_like,
) = comment::table
.find(comment_id)
.inner_join(person::table)
// recipient here
.left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
.left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
.inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(comment_aggregates::table)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id)),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
comment_saved::table.on(
comment::id
.eq(comment_saved::comment_id)
.and(comment_saved::person_id.eq(person_id_join)),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(person_id_join)),
),
)
.select((
comment::all_columns,
Person::safe_columns_tuple(),
comment_alias_1::all_columns.nullable(),
PersonAlias1::safe_columns_tuple().nullable(),
post::all_columns,
Community::safe_columns_tuple(),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
community_follower::all_columns.nullable(),
comment_saved::all_columns.nullable(),
comment_like::score.nullable(),
))
.first::<CommentViewTuple>(conn)?;
// If a person is given, then my_vote, if None, should be 0, not null
// Necessary to differentiate between other person's votes
let my_vote = if my_person_id.is_some() && comment_like.is_none() {
Some(0)
} else {
comment_like
};
Ok(CommentView {
comment,
recipient,
post,
creator,
community,
counts,
creator_banned_from_community: creator_banned_from_community.is_some(),
subscribed: subscribed.is_some(),
saved: saved.is_some(),
my_vote,
})
}
/// Gets the recipient person id.
/// If there is no parent comment, its the post creator
pub fn get_recipient_id(&self) -> PersonId {
match &self.recipient {
Some(parent_commenter) => parent_commenter.id,
None => self.post.creator_id,
}
}
}
pub struct CommentQueryBuilder<'a> {
conn: &'a PgConnection,
listing_type: ListingType,
sort: &'a SortType,
community_id: Option<CommunityId>,
community_name: Option<String>,
post_id: Option<PostId>,
creator_id: Option<PersonId>,
recipient_id: Option<PersonId>,
my_person_id: Option<PersonId>,
search_term: Option<String>,
saved_only: bool,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
}
impl<'a> CommentQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self {
CommentQueryBuilder {
conn,
listing_type: ListingType::All,
sort: &SortType::New,
community_id: None,
community_name: None,
post_id: None,
creator_id: None,
recipient_id: None,
my_person_id: None,
search_term: None,
saved_only: false,
unread_only: false,
page: None,
limit: None,
}
}
pub fn listing_type(mut self, listing_type: ListingType) -> Self {
self.listing_type = listing_type;
self
}
pub fn sort(mut self, sort: &'a SortType) -> Self {
self.sort = sort;
self
}
pub fn post_id<T: MaybeOptional<PostId>>(mut self, post_id: T) -> Self {
self.post_id = post_id.get_optional();
self
}
pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
self.creator_id = creator_id.get_optional();
self
}
pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self {
self.recipient_id = recipient_id.get_optional();
self
}
pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
self.community_id = community_id.get_optional();
self
}
pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
self.my_person_id = my_person_id.get_optional();
self
}
pub fn community_name<T: MaybeOptional<String>>(mut self, community_name: T) -> Self {
self.community_name = community_name.get_optional();
self
}
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
self.search_term = search_term.get_optional();
self
}
pub fn saved_only(mut self, saved_only: bool) -> Self {
self.saved_only = saved_only;
self
}
pub fn unread_only(mut self, unread_only: bool) -> Self {
self.unread_only = unread_only;
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<CommentView>, Error> {
use diesel::dsl::*;
// The left join below will return None in this case
let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
let mut query = comment::table
.inner_join(person::table)
// recipient here
.left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
.left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
.inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(comment_aggregates::table)
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id)),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
comment_saved::table.on(
comment::id
.eq(comment_saved::comment_id)
.and(comment_saved::person_id.eq(person_id_join)),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(person_id_join)),
),
)
.select((
comment::all_columns,
Person::safe_columns_tuple(),
comment_alias_1::all_columns.nullable(),
PersonAlias1::safe_columns_tuple().nullable(),
post::all_columns,
Community::safe_columns_tuple(),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
community_follower::all_columns.nullable(),
comment_saved::all_columns.nullable(),
comment_like::score.nullable(),
))
.into_boxed();
// The replies
if let Some(recipient_id) = self.recipient_id {
query = query
// TODO needs lots of testing
.filter(person_alias_1::id.eq(recipient_id)) // Gets the comment replies
.or_filter(
comment::parent_id
.is_null()
.and(post::creator_id.eq(recipient_id)),
) // Gets the top level replies
.filter(comment::deleted.eq(false))
.filter(comment::removed.eq(false));
}
if self.unread_only {
query = query.filter(comment::read.eq(false));
}
if let Some(creator_id) = self.creator_id {
query = query.filter(comment::creator_id.eq(creator_id));
};
if let Some(community_id) = self.community_id {
query = query.filter(post::community_id.eq(community_id));
}
if let Some(community_name) = self.community_name {
query = query
.filter(community::name.eq(community_name))
.filter(comment::local.eq(true));
}
if let Some(post_id) = self.post_id {
query = query.filter(comment::post_id.eq(post_id));
};
if let Some(search_term) = self.search_term {
query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
};
query = match self.listing_type {
// ListingType::Subscribed => query.filter(community_follower::subscribed.eq(true)),
ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
ListingType::Local => query.filter(community::local.eq(true)),
_ => query,
};
if self.saved_only {
query = query.filter(comment_saved::id.is_not_null());
}
query = match self.sort {
SortType::Hot | SortType::Active => query
.order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => {
query.order_by(comment::published.desc())
}
SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
SortType::TopYear => query
.filter(comment::published.gt(now - 1.years()))
.order_by(comment_aggregates::score.desc()),
SortType::TopMonth => query
.filter(comment::published.gt(now - 1.months()))
.order_by(comment_aggregates::score.desc()),
SortType::TopWeek => query
.filter(comment::published.gt(now - 1.weeks()))
.order_by(comment_aggregates::score.desc()),
SortType::TopDay => query
.filter(comment::published.gt(now - 1.days()))
.order_by(comment_aggregates::score.desc()),
};
let (limit, offset) = limit_and_offset(self.page, self.limit);
// Note: deleted and removed comments are done on the front side
let res = query
.limit(limit)
.offset(offset)
.load::<CommentViewTuple>(self.conn)?;
Ok(CommentView::from_tuple_to_vec(res))
}
}
impl ViewToVec for CommentView {
type DbTuple = CommentViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
comment: a.0.to_owned(),
creator: a.1.to_owned(),
recipient: a.3.to_owned(),
post: a.4.to_owned(),
community: a.5.to_owned(),
counts: a.6.to_owned(),
creator_banned_from_community: a.7.is_some(),
subscribed: a.8.is_some(),
saved: a.9.is_some(),
my_vote: a.10,
})
.collect::<Vec<Self>>()
}
}
#[cfg(test)]
mod tests {
use crate::comment_view::*;
use lemmy_db_queries::{
aggregates::comment_aggregates::CommentAggregates,
establish_unpooled_connection,
Crud,
Likeable,
};
use lemmy_db_schema::source::{comment::*, community::*, person::*, post::*};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "timmy".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_community = CommunityForm {
name: "test community 5".to_string(),
title: "nada".to_owned(),
description: None,
creator_id: inserted_person.id,
removed: None,
deleted: None,
updated: None,
nsfw: false,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post 2".into(),
creator_id: inserted_person.id,
url: None,
body: None,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
updated: None,
nsfw: false,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment 32".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
parent_id: None,
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
let expected_comment_view_no_person = CommentView {
creator_banned_from_community: false,
my_vote: None,
subscribed: false,
saved: false,
comment: Comment {
id: inserted_comment.id,
content: "A test comment 32".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
parent_id: None,
removed: false,
deleted: false,
read: false,
published: inserted_comment.published,
ap_id: inserted_comment.ap_id,
updated: None,
local: true,
},
creator: PersonSafe {
id: inserted_person.id,
name: "timmy".into(),
preferred_username: None,
published: inserted_person.published,
avatar: None,
actor_id: inserted_person.actor_id.to_owned(),
local: true,
banned: false,
deleted: false,
bio: None,
banner: None,
updated: None,
inbox_url: inserted_person.inbox_url.to_owned(),
shared_inbox_url: None,
},
recipient: None,
post: Post {
id: inserted_post.id,
name: inserted_post.name.to_owned(),
creator_id: inserted_person.id,
url: None,
body: None,
published: inserted_post.published,
updated: None,
community_id: inserted_community.id,
removed: false,
deleted: false,
locked: false,
stickied: false,
nsfw: false,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: inserted_post.ap_id.to_owned(),
local: true,
},
community: CommunitySafe {
id: inserted_community.id,
name: "test community 5".to_string(),
icon: None,
removed: false,
deleted: false,
nsfw: false,
actor_id: inserted_community.actor_id.to_owned(),
local: true,
title: "nada".to_owned(),
description: None,
creator_id: inserted_person.id,
updated: None,
banner: None,
published: inserted_community.published,
},
counts: CommentAggregates {
id: agg.id,
comment_id: inserted_comment.id,
score: 1,
upvotes: 1,
downvotes: 0,
published: agg.published,
},
};
let mut expected_comment_view_with_person = expected_comment_view_no_person.to_owned();
expected_comment_view_with_person.my_vote = Some(1);
let read_comment_views_no_person = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.list()
.unwrap();
let read_comment_views_with_person = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.my_person_id(inserted_person.id)
.list()
.unwrap();
let like_removed = CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(
expected_comment_view_no_person,
read_comment_views_no_person[0]
);
assert_eq!(
expected_comment_view_with_person,
read_comment_views_with_person[0]
);
assert_eq!(1, num_deleted);
assert_eq!(1, like_removed);
}
}

View file

@ -1,10 +0,0 @@
#[cfg(test)]
extern crate serial_test;
pub mod comment_report_view;
pub mod comment_view;
pub mod local_user_view;
pub mod post_report_view;
pub mod post_view;
pub mod private_message_view;
pub mod site_view;

View file

@ -1,147 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{aggregates::person_aggregates::PersonAggregates, ToSafe, ToSafeSettings};
use lemmy_db_schema::{
schema::{local_user, person, person_aggregates},
source::{
local_user::{LocalUser, LocalUserSettings},
person::{Person, PersonSafe},
},
LocalUserId,
PersonId,
};
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct LocalUserView {
pub local_user: LocalUser,
pub person: Person,
pub counts: PersonAggregates,
}
type LocalUserViewTuple = (LocalUser, Person, PersonAggregates);
impl LocalUserView {
pub fn read(conn: &PgConnection, local_user_id: LocalUserId) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.find(local_user_id)
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.select((
local_user::all_columns,
person::all_columns,
person_aggregates::all_columns,
))
.first::<LocalUserViewTuple>(conn)?;
Ok(Self {
local_user,
person,
counts,
})
}
pub fn read_person(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.filter(person::id.eq(person_id))
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.select((
local_user::all_columns,
person::all_columns,
person_aggregates::all_columns,
))
.first::<LocalUserViewTuple>(conn)?;
Ok(Self {
local_user,
person,
counts,
})
}
// TODO check where this is used
pub fn read_from_name(conn: &PgConnection, name: &str) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.filter(person::name.eq(name))
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.select((
local_user::all_columns,
person::all_columns,
person_aggregates::all_columns,
))
.first::<LocalUserViewTuple>(conn)?;
Ok(Self {
person,
counts,
local_user,
})
}
pub fn find_by_email_or_name(conn: &PgConnection, name_or_email: &str) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.filter(
person::name
.ilike(name_or_email)
.or(local_user::email.ilike(name_or_email)),
)
.select((
local_user::all_columns,
person::all_columns,
person_aggregates::all_columns,
))
.first::<LocalUserViewTuple>(conn)?;
Ok(Self {
person,
counts,
local_user,
})
}
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.filter(local_user::email.eq(from_email))
.select((
local_user::all_columns,
person::all_columns,
person_aggregates::all_columns,
))
.first::<LocalUserViewTuple>(conn)?;
Ok(Self {
person,
counts,
local_user,
})
}
}
#[derive(Debug, Serialize, Clone)]
pub struct LocalUserSettingsView {
pub local_user: LocalUserSettings,
pub person: PersonSafe,
pub counts: PersonAggregates,
}
type LocalUserSettingsViewTuple = (LocalUserSettings, PersonSafe, PersonAggregates);
impl LocalUserSettingsView {
pub fn read(conn: &PgConnection, local_user_id: LocalUserId) -> Result<Self, Error> {
let (local_user, person, counts) = local_user::table
.find(local_user_id)
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.select((
LocalUser::safe_settings_columns_tuple(),
Person::safe_columns_tuple(),
person_aggregates::all_columns,
))
.first::<LocalUserSettingsViewTuple>(conn)?;
Ok(Self {
person,
counts,
local_user,
})
}
}

View file

@ -1,183 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{limit_and_offset, MaybeOptional, ToSafe, ViewToVec};
use lemmy_db_schema::{
schema::{community, person, person_alias_1, person_alias_2, post, post_report},
source::{
community::{Community, CommunitySafe},
person::{Person, PersonAlias1, PersonAlias2, PersonSafe, PersonSafeAlias1, PersonSafeAlias2},
post::Post,
post_report::PostReport,
},
CommunityId,
};
use serde::Serialize;
#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct PostReportView {
pub post_report: PostReport,
pub post: Post,
pub community: CommunitySafe,
pub creator: PersonSafe,
pub post_creator: PersonSafeAlias1,
pub resolver: Option<PersonSafeAlias2>,
}
type PostReportViewTuple = (
PostReport,
Post,
CommunitySafe,
PersonSafe,
PersonSafeAlias1,
Option<PersonSafeAlias2>,
);
impl PostReportView {
/// returns the PostReportView for the provided report_id
///
/// * `report_id` - the report id to obtain
pub fn read(conn: &PgConnection, report_id: i32) -> Result<Self, Error> {
let (post_report, post, community, creator, post_creator, resolver) = post_report::table
.find(report_id)
.inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(post_report::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(post::creator_id.eq(person_alias_1::id)))
.left_join(
person_alias_2::table.on(post_report::resolver_id.eq(person_alias_2::id.nullable())),
)
.select((
post_report::all_columns,
post::all_columns,
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
PersonAlias2::safe_columns_tuple().nullable(),
))
.first::<PostReportViewTuple>(conn)?;
Ok(Self {
post_report,
post,
community,
creator,
post_creator,
resolver,
})
}
/// returns the current unresolved post report count for the supplied community ids
///
/// * `community_ids` - a Vec<i32> of community_ids to get a count for
/// TODO this eq_any is a bad way to do this, would be better to join to communitymoderator
/// for a person id
pub fn get_report_count(
conn: &PgConnection,
community_ids: &[CommunityId],
) -> Result<i64, Error> {
use diesel::dsl::*;
post_report::table
.inner_join(post::table)
.filter(
post_report::resolved
.eq(false)
.and(post::community_id.eq_any(community_ids)),
)
.select(count(post_report::id))
.first::<i64>(conn)
}
}
pub struct PostReportQueryBuilder<'a> {
conn: &'a PgConnection,
community_ids: Option<Vec<CommunityId>>, // TODO bad way to do this
page: Option<i64>,
limit: Option<i64>,
resolved: Option<bool>,
}
impl<'a> PostReportQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self {
PostReportQueryBuilder {
conn,
community_ids: None,
page: None,
limit: None,
resolved: Some(false),
}
}
pub fn community_ids<T: MaybeOptional<Vec<CommunityId>>>(mut self, community_ids: T) -> Self {
self.community_ids = community_ids.get_optional();
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn resolved<T: MaybeOptional<bool>>(mut self, resolved: T) -> Self {
self.resolved = resolved.get_optional();
self
}
pub fn list(self) -> Result<Vec<PostReportView>, Error> {
let mut query = post_report::table
.inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person::table.on(post_report::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(post::creator_id.eq(person_alias_1::id)))
.left_join(
person_alias_2::table.on(post_report::resolver_id.eq(person_alias_2::id.nullable())),
)
.select((
post_report::all_columns,
post::all_columns,
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
PersonAlias2::safe_columns_tuple().nullable(),
))
.into_boxed();
if let Some(comm_ids) = self.community_ids {
query = query.filter(post::community_id.eq_any(comm_ids));
}
if let Some(resolved_flag) = self.resolved {
query = query.filter(post_report::resolved.eq(resolved_flag));
}
let (limit, offset) = limit_and_offset(self.page, self.limit);
let res = query
.order_by(post_report::published.asc())
.limit(limit)
.offset(offset)
.load::<PostReportViewTuple>(self.conn)?;
Ok(PostReportView::from_tuple_to_vec(res))
}
}
impl ViewToVec for PostReportView {
type DbTuple = PostReportViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
post_report: a.0.to_owned(),
post: a.1.to_owned(),
community: a.2.to_owned(),
creator: a.3.to_owned(),
post_creator: a.4.to_owned(),
resolver: a.5.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -1,668 +0,0 @@
use diesel::{pg::Pg, result::Error, *};
use lemmy_db_queries::{
aggregates::post_aggregates::PostAggregates,
functions::hot_rank,
fuzzy_search,
limit_and_offset,
ListingType,
MaybeOptional,
SortType,
ToSafe,
ViewToVec,
};
use lemmy_db_schema::{
schema::{
community,
community_follower,
community_person_ban,
person,
post,
post_aggregates,
post_like,
post_read,
post_saved,
},
source::{
community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
person::{Person, PersonSafe},
post::{Post, PostRead, PostSaved},
},
CommunityId,
PersonId,
PostId,
};
use log::debug;
use serde::Serialize;
#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct PostView {
pub post: Post,
pub creator: PersonSafe,
pub community: CommunitySafe,
pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
pub counts: PostAggregates,
pub subscribed: bool, // Left join to CommunityFollower
pub saved: bool, // Left join to PostSaved
pub read: bool, // Left join to PostRead
pub my_vote: Option<i16>, // Left join to PostLike
}
type PostViewTuple = (
Post,
PersonSafe,
CommunitySafe,
Option<CommunityPersonBan>,
PostAggregates,
Option<CommunityFollower>,
Option<PostSaved>,
Option<PostRead>,
Option<i16>,
);
impl PostView {
pub fn read(
conn: &PgConnection,
post_id: PostId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
// The left join below will return None in this case
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
let (
post,
creator,
community,
creator_banned_from_community,
counts,
follower,
saved,
read,
post_like,
) = post::table
.find(post_id)
.inner_join(person::table)
.inner_join(community::table)
.left_join(
community_person_ban::table.on(
post::community_id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(post::creator_id)),
),
)
.inner_join(post_aggregates::table)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
post_saved::table.on(
post::id
.eq(post_saved::post_id)
.and(post_saved::person_id.eq(person_id_join)),
),
)
.left_join(
post_read::table.on(
post::id
.eq(post_read::post_id)
.and(post_read::person_id.eq(person_id_join)),
),
)
.left_join(
post_like::table.on(
post::id
.eq(post_like::post_id)
.and(post_like::person_id.eq(person_id_join)),
),
)
.select((
post::all_columns,
Person::safe_columns_tuple(),
Community::safe_columns_tuple(),
community_person_ban::all_columns.nullable(),
post_aggregates::all_columns,
community_follower::all_columns.nullable(),
post_saved::all_columns.nullable(),
post_read::all_columns.nullable(),
post_like::score.nullable(),
))
.first::<PostViewTuple>(conn)?;
// If a person is given, then my_vote, if None, should be 0, not null
// Necessary to differentiate between other person's votes
let my_vote = if my_person_id.is_some() && post_like.is_none() {
Some(0)
} else {
post_like
};
Ok(PostView {
post,
creator,
community,
creator_banned_from_community: creator_banned_from_community.is_some(),
counts,
subscribed: follower.is_some(),
saved: saved.is_some(),
read: read.is_some(),
my_vote,
})
}
}
pub struct PostQueryBuilder<'a> {
conn: &'a PgConnection,
listing_type: &'a ListingType,
sort: &'a SortType,
creator_id: Option<PersonId>,
community_id: Option<CommunityId>,
community_name: Option<String>,
my_person_id: Option<PersonId>,
search_term: Option<String>,
url_search: Option<String>,
show_nsfw: bool,
saved_only: bool,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
}
impl<'a> PostQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self {
PostQueryBuilder {
conn,
listing_type: &ListingType::All,
sort: &SortType::Hot,
creator_id: None,
community_id: None,
community_name: None,
my_person_id: None,
search_term: None,
url_search: None,
show_nsfw: true,
saved_only: false,
unread_only: false,
page: None,
limit: None,
}
}
pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
self.listing_type = listing_type;
self
}
pub fn sort(mut self, sort: &'a SortType) -> Self {
self.sort = sort;
self
}
pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
self.community_id = community_id.get_optional();
self
}
pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
self.my_person_id = my_person_id.get_optional();
self
}
pub fn community_name<T: MaybeOptional<String>>(mut self, community_name: T) -> Self {
self.community_name = community_name.get_optional();
self
}
pub fn creator_id<T: MaybeOptional<PersonId>>(mut self, creator_id: T) -> Self {
self.creator_id = creator_id.get_optional();
self
}
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
self.search_term = search_term.get_optional();
self
}
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
self.url_search = url_search.get_optional();
self
}
pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
self.show_nsfw = show_nsfw;
self
}
pub fn saved_only(mut self, saved_only: bool) -> Self {
self.saved_only = saved_only;
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<PostView>, Error> {
use diesel::dsl::*;
// The left join below will return None in this case
let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
let mut query = post::table
.inner_join(person::table)
.inner_join(community::table)
.left_join(
community_person_ban::table.on(
post::community_id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(community::creator_id)),
),
)
.inner_join(post_aggregates::table)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
post_saved::table.on(
post::id
.eq(post_saved::post_id)
.and(post_saved::person_id.eq(person_id_join)),
),
)
.left_join(
post_read::table.on(
post::id
.eq(post_read::post_id)
.and(post_read::person_id.eq(person_id_join)),
),
)
.left_join(
post_like::table.on(
post::id
.eq(post_like::post_id)
.and(post_like::person_id.eq(person_id_join)),
),
)
.select((
post::all_columns,
Person::safe_columns_tuple(),
Community::safe_columns_tuple(),
community_person_ban::all_columns.nullable(),
post_aggregates::all_columns,
community_follower::all_columns.nullable(),
post_saved::all_columns.nullable(),
post_read::all_columns.nullable(),
post_like::score.nullable(),
))
.into_boxed();
query = match self.listing_type {
ListingType::Subscribed => query.filter(community_follower::person_id.is_not_null()), // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
ListingType::Local => query.filter(community::local.eq(true)),
_ => query,
};
if let Some(community_id) = self.community_id {
query = query
.filter(post::community_id.eq(community_id))
.then_order_by(post_aggregates::stickied.desc());
}
if let Some(community_name) = self.community_name {
query = query
.filter(community::name.eq(community_name))
.filter(community::local.eq(true))
.then_order_by(post_aggregates::stickied.desc());
}
if let Some(url_search) = self.url_search {
query = query.filter(post::url.eq(url_search));
}
if let Some(search_term) = self.search_term {
let searcher = fuzzy_search(&search_term);
query = query.filter(
post::name
.ilike(searcher.to_owned())
.or(post::body.ilike(searcher)),
);
}
// If its for a specific person, show the removed / deleted
if let Some(creator_id) = self.creator_id {
query = query.filter(post::creator_id.eq(creator_id));
}
if !self.show_nsfw {
query = query
.filter(post::nsfw.eq(false))
.filter(community::nsfw.eq(false));
};
// TODO These two might be wrong
if self.saved_only {
query = query.filter(post_saved::id.is_not_null());
};
if self.unread_only {
query = query.filter(post_read::id.is_not_null());
};
query = match self.sort {
SortType::Active => query
.then_order_by(
hot_rank(
post_aggregates::score,
post_aggregates::newest_comment_time_necro,
)
.desc(),
)
.then_order_by(post_aggregates::newest_comment_time_necro.desc()),
SortType::Hot => query
.then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
.then_order_by(post_aggregates::published.desc()),
SortType::New => query.then_order_by(post_aggregates::published.desc()),
SortType::MostComments => query.then_order_by(post_aggregates::comments.desc()),
SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
SortType::TopYear => query
.filter(post::published.gt(now - 1.years()))
.then_order_by(post_aggregates::score.desc()),
SortType::TopMonth => query
.filter(post::published.gt(now - 1.months()))
.then_order_by(post_aggregates::score.desc()),
SortType::TopWeek => query
.filter(post::published.gt(now - 1.weeks()))
.then_order_by(post_aggregates::score.desc()),
SortType::TopDay => query
.filter(post::published.gt(now - 1.days()))
.then_order_by(post_aggregates::score.desc()),
};
let (limit, offset) = limit_and_offset(self.page, self.limit);
query = query
.limit(limit)
.offset(offset)
.filter(post::removed.eq(false))
.filter(post::deleted.eq(false))
.filter(community::removed.eq(false))
.filter(community::deleted.eq(false));
debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
let res = query.load::<PostViewTuple>(self.conn)?;
Ok(PostView::from_tuple_to_vec(res))
}
}
impl ViewToVec for PostView {
type DbTuple = PostViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
post: a.0.to_owned(),
creator: a.1.to_owned(),
community: a.2.to_owned(),
creator_banned_from_community: a.3.is_some(),
counts: a.4.to_owned(),
subscribed: a.5.is_some(),
saved: a.6.is_some(),
read: a.7.is_some(),
my_vote: a.8,
})
.collect::<Vec<Self>>()
}
}
#[cfg(test)]
mod tests {
use crate::post_view::{PostQueryBuilder, PostView};
use lemmy_db_queries::{
aggregates::post_aggregates::PostAggregates,
establish_unpooled_connection,
Crud,
Likeable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{community::*, person::*, post::*};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let person_name = "tegan".to_string();
let community_name = "test_community_3".to_string();
let post_name = "test post 3".to_string();
let new_person = PersonForm {
name: person_name.to_owned(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_community = CommunityForm {
name: community_name.to_owned(),
title: "nada".to_owned(),
description: None,
creator_id: inserted_person.id,
removed: None,
deleted: None,
updated: None,
nsfw: false,
actor_id: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: post_name.to_owned(),
url: None,
body: None,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
locked: None,
stickied: None,
updated: None,
nsfw: false,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: None,
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let post_like_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
let expected_post_like = PostLike {
id: inserted_post_like.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
published: inserted_post_like.published,
score: 1,
};
let read_post_listings_with_person = PostQueryBuilder::create(&conn)
.listing_type(&ListingType::Community)
.sort(&SortType::New)
.community_id(inserted_community.id)
.my_person_id(inserted_person.id)
.list()
.unwrap();
let read_post_listings_no_person = PostQueryBuilder::create(&conn)
.listing_type(&ListingType::Community)
.sort(&SortType::New)
.community_id(inserted_community.id)
.list()
.unwrap();
let read_post_listing_no_person = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_person =
PostView::read(&conn, inserted_post.id, Some(inserted_person.id)).unwrap();
let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
// the non person version
let expected_post_listing_no_person = PostView {
post: Post {
id: inserted_post.id,
name: post_name,
creator_id: inserted_person.id,
url: None,
body: None,
published: inserted_post.published,
updated: None,
community_id: inserted_community.id,
removed: false,
deleted: false,
locked: false,
stickied: false,
nsfw: false,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: inserted_post.ap_id.to_owned(),
local: true,
},
my_vote: None,
creator: PersonSafe {
id: inserted_person.id,
name: person_name,
preferred_username: None,
published: inserted_person.published,
avatar: None,
actor_id: inserted_person.actor_id.to_owned(),
local: true,
banned: false,
deleted: false,
bio: None,
banner: None,
updated: None,
inbox_url: inserted_person.inbox_url.to_owned(),
shared_inbox_url: None,
},
creator_banned_from_community: false,
community: CommunitySafe {
id: inserted_community.id,
name: community_name,
icon: None,
removed: false,
deleted: false,
nsfw: false,
actor_id: inserted_community.actor_id.to_owned(),
local: true,
title: "nada".to_owned(),
description: None,
creator_id: inserted_person.id,
updated: None,
banner: None,
published: inserted_community.published,
},
counts: PostAggregates {
id: agg.id,
post_id: inserted_post.id,
comments: 0,
score: 1,
upvotes: 1,
downvotes: 0,
stickied: false,
published: agg.published,
newest_comment_time_necro: inserted_post.published,
newest_comment_time: inserted_post.published,
},
subscribed: false,
read: false,
saved: false,
};
// TODO More needs to be added here
let mut expected_post_listing_with_user = expected_post_listing_no_person.to_owned();
expected_post_listing_with_user.my_vote = Some(1);
let like_removed = PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
// The with user
assert_eq!(
expected_post_listing_with_user,
read_post_listings_with_person[0]
);
assert_eq!(
expected_post_listing_with_user,
read_post_listing_with_person
);
assert_eq!(1, read_post_listings_with_person.len());
// Without the user
assert_eq!(
expected_post_listing_no_person,
read_post_listings_no_person[0]
);
assert_eq!(expected_post_listing_no_person, read_post_listing_no_person);
assert_eq!(1, read_post_listings_no_person.len());
// assert_eq!(expected_post, inserted_post);
// assert_eq!(expected_post, updated_post);
assert_eq!(expected_post_like, inserted_post_like);
assert_eq!(1, like_removed);
assert_eq!(1, num_deleted);
}
}

View file

@ -1,137 +0,0 @@
use diesel::{pg::Pg, result::Error, *};
use lemmy_db_queries::{limit_and_offset, MaybeOptional, ToSafe, ViewToVec};
use lemmy_db_schema::{
schema::{person, person_alias_1, private_message},
source::{
person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1},
private_message::PrivateMessage,
},
PersonId,
PrivateMessageId,
};
use log::debug;
use serde::Serialize;
#[derive(Debug, PartialEq, Serialize, Clone)]
pub struct PrivateMessageView {
pub private_message: PrivateMessage,
pub creator: PersonSafe,
pub recipient: PersonSafeAlias1,
}
type PrivateMessageViewTuple = (PrivateMessage, PersonSafe, PersonSafeAlias1);
impl PrivateMessageView {
pub fn read(conn: &PgConnection, private_message_id: PrivateMessageId) -> Result<Self, Error> {
let (private_message, creator, recipient) = private_message::table
.find(private_message_id)
.inner_join(person::table.on(private_message::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(private_message::recipient_id.eq(person_alias_1::id)))
.order_by(private_message::published.desc())
.select((
private_message::all_columns,
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
))
.first::<PrivateMessageViewTuple>(conn)?;
Ok(PrivateMessageView {
private_message,
creator,
recipient,
})
}
}
pub struct PrivateMessageQueryBuilder<'a> {
conn: &'a PgConnection,
recipient_id: PersonId,
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
}
impl<'a> PrivateMessageQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, recipient_id: PersonId) -> Self {
PrivateMessageQueryBuilder {
conn,
recipient_id,
unread_only: false,
page: None,
limit: None,
}
}
pub fn unread_only(mut self, unread_only: bool) -> Self {
self.unread_only = unread_only;
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
let mut query = private_message::table
.inner_join(person::table.on(private_message::creator_id.eq(person::id)))
.inner_join(person_alias_1::table.on(private_message::recipient_id.eq(person_alias_1::id)))
.select((
private_message::all_columns,
Person::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
))
.into_boxed();
// If its unread, I only want the ones to me
if self.unread_only {
query = query
.filter(private_message::read.eq(false))
.filter(private_message::recipient_id.eq(self.recipient_id));
}
// Otherwise, I want the ALL view to show both sent and received
else {
query = query.filter(
private_message::recipient_id
.eq(self.recipient_id)
.or(private_message::creator_id.eq(self.recipient_id)),
)
}
let (limit, offset) = limit_and_offset(self.page, self.limit);
query = query
.filter(private_message::deleted.eq(false))
.limit(limit)
.offset(offset)
.order_by(private_message::published.desc());
debug!(
"Private Message View Query: {:?}",
debug_query::<Pg, _>(&query)
);
let res = query.load::<PrivateMessageViewTuple>(self.conn)?;
Ok(PrivateMessageView::from_tuple_to_vec(res))
}
}
impl ViewToVec for PrivateMessageView {
type DbTuple = PrivateMessageViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
private_message: a.0.to_owned(),
creator: a.1.to_owned(),
recipient: a.2.to_owned(),
})
.collect::<Vec<Self>>()
}
}

View file

@ -1,37 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{aggregates::site_aggregates::SiteAggregates, ToSafe};
use lemmy_db_schema::{
schema::{person, site, site_aggregates},
source::{
person::{Person, PersonSafe},
site::Site,
},
};
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct SiteView {
pub site: Site,
pub creator: PersonSafe,
pub counts: SiteAggregates,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
let (site, creator, counts) = site::table
.inner_join(person::table)
.inner_join(site_aggregates::table)
.select((
site::all_columns,
Person::safe_columns_tuple(),
site_aggregates::all_columns,
))
.first::<(Site, PersonSafe, SiteAggregates)>(conn)?;
Ok(SiteView {
site,
creator,
counts,
})
}
}

View file

@ -1,13 +0,0 @@
[package]
name = "lemmy_db_views_actor"
version = "0.1.0"
edition = "2018"
[lib]
doctest = false
[dependencies]
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
diesel = { version = "1.4.5", features = ["postgres","chrono","r2d2","serde_json"] }
serde = { version = "1.0.123", features = ["derive"] }

View file

@ -1,65 +0,0 @@
use diesel::{result::Error, *};
use lemmy_db_queries::{ToSafe, ViewToVec};
use lemmy_db_schema::{
schema::{community, community_follower, person},
source::{
community::{Community, CommunitySafe},
person::{Person, PersonSafe},
},
CommunityId,
PersonId,
};
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct CommunityFollowerView {
pub community: CommunitySafe,
pub follower: PersonSafe,
}
type CommunityFollowerViewTuple = (CommunitySafe, PersonSafe);
impl CommunityFollowerView {
pub fn for_community(conn: &PgConnection, community_id: CommunityId) -> Result<Vec<Self>, Error> {
let res = community_follower::table
.inner_join(community::table)
.inner_join(person::table)
.select((
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
))
.filter(community_follower::community_id.eq(community_id))
.order_by(community_follower::published)
.load::<CommunityFollowerViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
pub fn for_person(conn: &PgConnection, person_id: PersonId) -> Result<Vec<Self>, Error> {
let res = community_follower::table
.inner_join(community::table)
.inner_join(person::table)
.select((
Community::safe_columns_tuple(),
Person::safe_columns_tuple(),
))
.filter(community_follower::person_id.eq(person_id))
.order_by(community_follower::published)
.load::<CommunityFollowerViewTuple>(conn)?;
Ok(Self::from_tuple_to_vec(res))
}
}
impl ViewToVec for CommunityFollowerView {
type DbTuple = CommunityFollowerViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.iter()
.map(|a| Self {
community: a.0.to_owned(),
follower: a.1.to_owned(),
})
.collect::<Vec<Self>>()
}
}

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