Compare commits

..

No commits in common. "main" and "delete-community_1" have entirely different histories.

580 changed files with 47446 additions and 45307 deletions

16
.dockerignore vendored
View file

@ -1,8 +1,12 @@
# 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 ui/node_modules
docker server/target
api_tests docker/dev/volumes
ansible docker/federation/volumes
tests docker/federation-test/volumes
.git .git
*.sh ansible
# exceptions, needed for federation-test build
!server/target/debug/lemmy_server

190
.drone.yml vendored
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

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
* linguist-vendored
*.rs linguist-vendored=false

View file

@ -1,29 +0,0 @@
---
name: "\U0001F41E Bug Report"
about: Create a report to help us improve Lemmy
title: ''
labels: bug
assignees: ''
---
Found a bug? Please fill out the sections below. 👍
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
### Issue Summary
A summary of the bug.
### Steps to Reproduce
1. (for example) I clicked login, and an endless spinner show up.
2. I tried to install lemmy via this guide, and I'm getting this error.
3. ...
### Technical details
* Please post your log: `sudo docker-compose logs > lemmy_log.out`.
* What OS are you trying to install lemmy on?
* Any browser console errors?

View file

@ -1,44 +0,0 @@
---
name: "\U0001F680 Feature request"
about: Suggest an idea for improving Lemmy
title: ''
labels: enhancement
assignees: ''
---
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
### Is your proposal related to a problem?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

View file

@ -1,10 +0,0 @@
---
name: "? Question"
about: General questions about Lemmy
title: ''
labels: question
assignees: ''
---
What's the question you have about lemmy?

17
.gitignore vendored
View file

@ -6,16 +6,13 @@ ansible/passwords/
# docker build files # docker build files
docker/lemmy_mine.hjson docker/lemmy_mine.hjson
docker/dev/env_deploy.sh docker/dev/env_deploy.sh
volumes docker/federation/volumes
docker/federation-test/volumes
# ide config docker/dev/volumes
.idea
.vscode
# local build files # local build files
target build/
env_setup.sh ui/src/translations
query_testing/**/reports/*.json
# API tests # ide config
api_tests/node_modules .idea/

1
.rgignore vendored
View file

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

5
.rustfmt.toml vendored
View file

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

34
.travis.yml vendored Normal file
View file

@ -0,0 +1,34 @@
language: rust
rust:
- stable
matrix:
allow_failures:
- rust: nightly
fast_finish: true
cache: cargo
before_cache:
- rm -rfv target/debug/incremental/lemmy_server-*
- rm -rfv target/debug/.fingerprint/lemmy_server-*
- rm -rfv target/debug/build/lemmy_server-*
- rm -rfv target/debug/deps/lemmy_server-*
- rm -rfv target/debug/lemmy_server.d
before_script:
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
- psql -c 'create database lemmy with owner lemmy;' -U postgres
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
before_install:
- cd server
script:
# Default checks, but fail if anything is detected
- cargo build
- cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
- cargo install diesel_cli --no-default-features --features postgres --force
- diesel migration run
- cargo test
env:
global:
- DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
- RUST_TEST_THREADS=1
addons:
postgresql: "9.4"

35
CODE_OF_CONDUCT.md vendored 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.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/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/).

2
CONTRIBUTING.md vendored
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.

62
Cargo.toml vendored
View file

@ -1,62 +0,0 @@
[package]
name = "lemmy_server"
version = "0.0.1"
edition = "2018"
[lib]
doctest = false
[profile.dev]
debug = 0
[workspace]
members = [
"crates/api",
"crates/apub",
"crates/utils",
"crates/db_queries",
"crates/db_schema",
"crates/db_views",
"crates/db_views_actor",
"crates/db_views_actor",
"crates/api_structs",
"crates/websocket",
"crates/routes"
]
[dependencies]
lemmy_api = { path = "./crates/api" }
lemmy_apub = { path = "./crates/apub" }
lemmy_utils = { path = "./crates/utils" }
lemmy_db_schema = { path = "./crates/db_schema" }
lemmy_db_queries = { path = "./crates/db_queries" }
lemmy_db_views = { path = "./crates/db_views" }
lemmy_db_views_moderator = { path = "./crates/db_views_moderator" }
lemmy_db_views_actor = { path = "./crates/db_views_actor" }
lemmy_api_structs = { path = "crates/api_structs" }
lemmy_websocket = { path = "./crates/websocket" }
lemmy_routes = { path = "./crates/routes" }
diesel = "1.4.5"
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] }
log = "0.4.14"
env_logger = "0.8.2"
strum = "0.20.0"
url = { version = "2.2.1", features = ["serde"] }
openssl = "0.10.32"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
tokio = "0.3.6"
anyhow = "1.0.38"
reqwest = { version = "0.10.10", features = ["json"] }
activitystreams = "0.7.0-alpha.10"
actix-rt = { version = "1.1.1", default-features = false }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
clokwerk = "0.3.4"
[dev-dependencies.cargo-husky]
version = "1.5.0"
default-features = false # Disable features which are enabled by default
features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"]

81
README.md vendored
View file

@ -1,53 +1,50 @@
<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=master)](https://travis-ci.org/LemmyNet/lemmy)
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues) [![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/) [![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
[![Translation status](http://weblate.yerbamate.ml/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.ml/engage/lemmy/) [![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE) [![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social) ![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
[![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">
<a href="https://join.lemmy.ml/" rel="noopener"> <a href="https://dev.lemmy.ml/" rel="noopener">
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmyNet/lemmy-ui/main/src/assets/icons/favicon.svg"></a> <img width=200px height=200px src="ui/assets/favicon.svg"></a>
<h3 align="center"><a href="https://join.lemmy.ml">Lemmy</a></h3> <h3 align="center"><a href="https://dev.lemmy.ml">Lemmy</a></h3>
<p align="center"> <p align="center">
A link aggregator / Reddit clone for the fediverse. A link aggregator / reddit clone for the fediverse.
<br /> <br />
<br /> <br />
<a href="https://join.lemmy.ml">Join Lemmy</a> <a href="https://dev.lemmy.ml">View Site</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/master/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://i.imgur.com/kZSRcRu.png)|![chat screen](https://i.imgur.com/4XghNh6.png)
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse). [Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere. For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
The overall goal is to create an easily self-hostable, decentralized alternative to Reddit and other link aggregators, outside of their corporate control and meddling. The overall goal is to create an easily self-hostable, decentralized alternative to reddit and other link aggregators, outside of their corporate control and meddling.
Each Lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing. Each lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
*Note: The WebSocket and HTTP APIs are currently unstable* *Note: Federation is still in active development*
### Why's it called Lemmy? ### Why's it called Lemmy?
@ -68,15 +65,15 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
- Open source, [AGPL License](/LICENSE). - Open source, [AGPL License](/LICENSE).
- Self hostable, easy to deploy. - Self hostable, easy to deploy.
- Comes with [Docker](https://join.lemmy.ml/docs/en/administration/install_docker.html) and [Ansible](https://join.lemmy.ml/docs/en/administration/install_ansible.html). - Comes with [Docker](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
- Clean, mobile-friendly interface. - Clean, mobile-friendly interface.
- Only a minimum of a username and password is required to sign up! - Only a minimum of a username and password is required to sign up!
- User avatar support. - User avatar support.
- Live-updating Comment threads. - Live-updating Comment threads.
- Full vote scores `(+/-)` like old Reddit. - Full vote scores `(+/-)` like old reddit.
- Themes, including light, dark, and solarized. - Themes, including light, dark, and solarized.
- Emojis with autocomplete support. Start typing `:` - Emojis with autocomplete support. Start typing `:`
- User tagging using `@`, Community tagging using `!`. - User tagging using `@`, Community tagging using `#`.
- Integrated image uploading in both posts and comments. - Integrated image uploading in both posts and comments.
- A post can consist of a title and any combination of self text, a URL, or nothing else. - A post can consist of a title and any combination of self text, a URL, or nothing else.
- Notifications, on comment replies and when you're tagged. - Notifications, on comment replies and when you're tagged.
@ -103,22 +100,9 @@ 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)
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
## Lemmy Projects
### Apps
- [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)
- [Remmel - A native iOS app](https://github.com/uuttff8/Lemmy-iOS)
### Libraries
- [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client)
- [Kotlin API ( under development )](https://github.com/eiknat/lemmy-client)
- [Dart API client ( under development )](https://github.com/krawieck/lemmy_api_client)
## Support / Donate ## Support / Donate
@ -127,7 +111,7 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
- [Support on Liberapay](https://liberapay.com/Lemmy). - [Support on Liberapay](https://liberapay.com/Lemmy).
- [Support on Patreon](https://www.patreon.com/dessalines). - [Support on Patreon](https://www.patreon.com/dessalines).
- [Support on OpenCollective](https://opencollective.com/lemmy). - [Support on OpenCollective](https://opencollective.com/lemmy).
- [List of Sponsors](https://join.lemmy.ml/sponsors). - [List of Sponsors](https://dev.lemmy.ml/sponsors).
### Crypto ### Crypto
@ -137,24 +121,21 @@ 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.dev/projects/lemmy/).
## Contact ## Contact
- [Mastodon](https://mastodon.social/@LemmyDev) - [Mastodon](https://mastodon.social/@LemmyDev) - [![Mastodon Follow](https://img.shields.io/mastodon/follow/810572?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@LemmyDev)
- [Matrix](https://matrix.to/#/#lemmy:matrix.org) - [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org) - [![Matrix](https://img.shields.io/matrix/rust-reddit-fediverse:matrix.org.svg?label=matrix-chat)](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
## Code Mirrors
- [GitHub](https://github.com/LemmyNet/lemmy) - [GitHub](https://github.com/LemmyNet/lemmy)
- [Gitea](https://yerbamate.ml/LemmyNet/lemmy) - [Gitea](https://yerbamate.dev/dessalines/lemmy)
- [Codeberg](https://codeberg.org/LemmyNet/lemmy) - [GitLab](https://gitlab.com/dessalines/lemmy)
## Credits ## Credits

309
RELEASES.md vendored
View file

@ -1,310 +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)
## Changes
We've been working at warp speed since our `v0.7.0` release in June, adding over [870 commits](https://github.com/LemmyNet/lemmy/compare/v0.7.0...main) since then. :sweat:
Here are some of the bigger changes:
### LemmyNet projects
- Created [LemmyNet](https://github.com/LemmyNet), where all lemmy-related projects live.
- Split out the frontend into a separete repository, [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
- Created a [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client), for any js / typescript developers.
- Split out i18n [lemmy-translations](https://github.com/LemmyNet/lemmy-translations), that any app or site developers can import and use. Lemmy currently supports [~30 languages!](https://weblate.yerbamate.ml/projects/lemmy/lemmy/)
### Lemmy Server
#### Federation
- The first **federation public beta release**, woohoo :fireworks:
- All Lemmy functionality now works over ActivityPub (except turning remote users into mods/admins)
- Instance allowlist and blocklist
- Documentation for [admins](https://join.lemmy.ml/docs/administration_federation.html) and [devs](https://join.lemmy.ml/docs/contributing_federation_overview.html) on how federation works
- Upgraded to newest versions of @asonix activitypub libraries
- Full local federation setup for manual testing
- Automated testing for nearly every federation action
- Many additional security checks
- Lots and lots of refactoring
- Asynchronous sending of outgoing activities
### User Interface
- Separated the UI from the server code, in [lemmy-ui](https://github.com/LemmyNet/lemmy-ui).
- The UI can now read with javascript disabled!
- It's now a fully isomorphic application using [inferno-isomorphic](https://infernojs.org/docs/guides/isomorphic). This means that page loads are now much faster, as the server does the work.
- The UI now also supports open-graph and twitter cards! Linking to lemmy posts (from whatever platform you use) looks pretty now: ![](https://i.imgur.com/6TZ2v7s.png)
- Improved the search page ( more features incoming ).
- The default view is now `Local`, instead of `All`, since all would show all federated posts.
- User settings are now shared across browsers ( a page refresh will pick up changes ).
- A much leaner mobile view.
#### Backend
- Re-organized the rust codebase into separate workspaces for backend and frontend.
- Removed materialized views, making the database **a lot faster**.
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
- New sort for `Local` ( meaning from local communities).
- Customizeable site, user, and community icons and banners.
- Added user preferred names / display names, bios, and cakedays.
- Visual / Audio captchas through the lemmy API.
- Lots of API field verifications.
- Upgraded to pictrs-v2 ( thanks to @asonix )
- Wayyy too many bugfixes to count.
## Contributors
We'd also like to thank both the [NLnet foundation](https://nlnet.nl/) for their support in allowing us to work full-time on Lemmy ( as well as their support for [other important open-source projects](https://nlnet.nl/project/current.html) ), [those who sponsor us](https://lemmy.ml/sponsors), and those who [help translate Lemmy](https://weblate.yerbamate.ml/projects/lemmy/). Every little bit does help. We remain committed to never allowing advertisements, monetizing, or venture-capital in Lemmy; software should be communal, and should benefit humanity, not a small group of company owners.
## Upgrading
- [with manual Docker installation](https://join.lemmy.ml/docs/administration_install_docker.html#updating)
- [with Ansible installation](https://join.lemmy.ml/docs/administration_install_ansible.html)
## Testing Federation
Federation is finally ready in Lemmy, pending possible bugs or other issues. So for now we suggest to enable federation only on test servers, or try it on our own test servers ( [enterprise](https://enterprise.lemmy.ml/), [ds9](https://ds9.lemmy.ml/), [voyager](https://voyager.lemmy.ml/) ).
If everything goes well, after a few weeks we will enable federation on lemmy.ml, at first with a limited number of trusted instances. We will also likely change the domain to https://lemmy.ml . Keep in mind that changing domains after turning on federation will break things.
To enable on your instance, edit your [lemmy.hjson](https://github.com/LemmyNet/lemmy/blob/main/config/defaults.hjson#L60) federation section to `enabled: true`, and restart.
### Connecting to another server
The server https://ds9.lemmy.ml has open federation, so after either adding it to the `allowed_instances` list in your `config.hjson`, or if you have open federation, you don't need to add it explicitly.
To federate / connect with a server, type in `!community_name@server.tld`, in your server's search box [like so](https://voyager.lemmy.ml/search/q/!main%40ds9.lemmy.ml/type/All/sort/TopAll/page/1).
To connect with the `main` community on ds9, the search is `!main@ds9.lemmy.ml`.
You can then click the community, and you will see a local version of the community, which you can subscribe to. New posts and comments from `!main@ds9.lemmy.ml` will now show up on your front page, or `/c/All`
# Lemmy v0.7.40 Pre-Release (2020-08-05)
We've [added a lot](https://github.com/LemmyNet/lemmy/compare/v0.7.40...v0.7.0) in this pre-release:
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
- Customizeable site icon and banner, user icon and banner, and community icon and banner.
- Added user preferred names / display names, bios, and cakedays.
- User settings are now shared across browsers (a page refresh will pick up changes).
- Visual / Audio captchas through the lemmy API.
- Lots of UI prettiness.
- Lots of bug fixes.
- Lots of additional translations.
- Lots of federation prepping / additions / refactors.
This release removes the need for you to have a pictrs nginx route (the requests are now routed through lemmy directly). Follow the upgrade instructions below to replace your nginx with the new one.
## Upgrading
**With Ansible:**
```
# run these commands locally
git pull
cd ansible
ansible-playbook lemmy.yml
```
**With manual Docker installation:**
```
# run these commands on your server
cd /lemmy
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
sudo nginx -s reload
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
sudo docker-compose up -d
```
# Lemmy v0.7.0 Release (2020-06-23)
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
with [pict-rs](https://git.asonix.dog/asonix/pict-rs), which improves performance
and security.
Overall, since our last major release in January (v0.6.0), we have closed over
[100 issues!](https://github.com/LemmyNet/lemmy/milestone/16?closed=1)
- Site-wide list of recent comments
- Reconnecting websockets
- Many more themes, including a default light one.
- Expandable embeds for post links (and thumbnails), from
[iframely](https://github.com/itteco/iframely)
- Better icons
- Emoji autocomplete to post and message bodies, and an Emoji Picker
- Post body now searchable
- Community title and description is now searchable
- Simplified cross-posts
- Better documentation
- LOTS more languages
- Lots of bugs squashed
- And more ...
## Upgrading
Before starting the upgrade, make sure that you have a working backup of your
database and image files. See our
[documentation](https://join.lemmy.ml/docs/administration_backup_and_restore.html)
for backup instructions.
**With Ansible:**
```
# deploy with ansible from your local lemmy git repo
git pull
cd ansible
ansible-playbook lemmy.yml
# connect via ssh to run the migration script
ssh your-server
cd /lemmy/
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
chmod +x migrate-pictshare-to-pictrs.bash
sudo ./migrate-pictshare-to-pictrs.bash
```
**With manual Docker installation:**
```
# run these commands on your server
cd /lemmy
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
sudo nginx -s reload
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
chmod +x migrate-pictshare-to-pictrs.bash
sudo bash migrate-pictshare-to-pictrs.bash
```
**Note:** After upgrading, all users need to reload the page, then logout and
login again, so that images are loaded correctly.
# Lemmy v0.6.0 Release (2020-01-16) # Lemmy v0.6.0 Release (2020-01-16)
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1) `v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)
@ -326,4 +19,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

2
ansible/VERSION vendored
View file

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

3
ansible/ansible.cfg vendored
View file

@ -1,6 +1,5 @@
[defaults] [defaults]
inventory = inventory inventory=inventory
interpreter_python = /usr/bin/python3
[ssh_connection] [ssh_connection]
pipelining = True pipelining = True

View file

@ -1,12 +1,6 @@
[lemmy] [lemmy]
# to get started, copy this file to `inventory` and adjust the values below. # define the username and hostname that you use for ssh connection, and specify the domain
# - `myuser@example.com`: replace with the destination you use to connect to your server via ssh myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com
# - `domain=example.com`: replace `example.com` with your lemmy domain
# - `letsencrypt_contact_email=your@email.com` replace `your@email.com` with your email address,
# to get notifications if your ssl cert expires
# - `lemmy_base_dir=/srv/lemmy`: the location on the server where lemmy can be installed, can be any folder
# if you are upgrading from a previous version, set this to `/lemmy`
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com lemmy_base_dir=/srv/lemmy
[all:vars] [all:vars]
ansible_connection=ssh ansible_connection=ssh

82
ansible/lemmy.yml vendored
View file

@ -5,41 +5,18 @@
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
gather_facts: False gather_facts: False
pre_tasks: pre_tasks:
- name: check lemmy_base_dir
fail:
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
when: lemmy_base_dir is not defined
- name: install python for Ansible - name: install python for Ansible
# python2-minimal instead of python-minimal for ubuntu 20.04 and up raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
raw: test -e /usr/bin/python || (apt -y update && apt install -y python3-minimal python3-setuptools)
args: args:
executable: /bin/bash executable: /bin/bash
register: output register: output
changed_when: output.stdout != '' changed_when: output.stdout != ""
- setup: # gather facts - setup: # gather facts
tasks: tasks:
- name: install dependencies - name: install dependencies
apt: apt:
pkg: pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
- 'nginx'
- 'docker-compose'
- 'docker.io'
- 'certbot'
- name: install certbot-nginx on ubuntu < 20
apt:
pkg:
- 'python-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '<')
- name: install certbot-nginx on ubuntu > 20
apt:
pkg:
- 'python3-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '>=')
- name: request initial letsencrypt certificate - name: request initial letsencrypt certificate
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
@ -47,50 +24,26 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: file: path={{item.path}} state=directory
path: '{{item.path}}'
owner: '{{item.owner}}'
state: directory
with_items: with_items:
- path: '{{lemmy_base_dir}}' - { path: '/lemmy/' }
owner: 'root' - { path: '/lemmy/volumes/' }
- path: '{{lemmy_base_dir}}/volumes/'
owner: 'root'
- path: '{{lemmy_base_dir}}/volumes/pictrs/'
owner: '991'
- block: - block:
- name: add template files - name: add template files
template: template: src={{item.src}} dest={{item.dest}} mode={{item.mode}}
src: '{{item.src}}'
dest: '{{item.dest}}'
mode: '{{item.mode}}'
with_items: with_items:
- src: 'templates/docker-compose.yml' - { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
dest: '{{lemmy_base_dir}}/docker-compose.yml' - { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
mode: '0600' - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
- src: 'templates/nginx.conf' vars:
dest: '/etc/nginx/sites-enabled/lemmy.conf'
mode: '0644'
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}" lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}"
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536" lemmy_port: "8536"
lemmy_ui_port: "1235"
pictshare_port: "8537" pictshare_port: "8537"
iframely_port: "8538" iframely_port: "8538"
- name: add config file (only during initial setup) - name: add config file (only during initial setup)
template: template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
src: 'templates/config.hjson'
dest: '{{lemmy_base_dir}}/lemmy.hjson'
mode: '0600'
force: false
owner: '1000'
group: '1000'
vars: vars:
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
@ -103,17 +56,16 @@
- name: start docker-compose - name: start docker-compose
docker_compose: docker_compose:
project_src: '{{lemmy_base_dir}}' project_src: /lemmy/
state: present state: present
pull: yes pull: yes
remove_orphans: yes
- name: reload nginx with new config - name: reload nginx with new config
shell: nginx -s reload shell: nginx -s reload
- name: certbot renewal cronjob - name: certbot renewal cronjob
cron: cron:
special_time: daily special_time=daily
name: certbot-renew-lemmy name=certbot-renew-lemmy
user: root user=root
job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"

94
ansible/lemmy_dev.yml vendored
View file

@ -1,34 +1,24 @@
--- ---
- hosts: all - hosts: all
vars: vars:
lemmy_docker_image: 'lemmy:dev' lemmy_docker_image: "lemmy:dev"
# Install python if required # Install python if required
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
gather_facts: False gather_facts: False
pre_tasks: pre_tasks:
- name: check lemmy_base_dir
fail:
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
when: lemmy_base_dir is not defined
- name: install python for Ansible - name: install python for Ansible
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools) raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
args: args:
executable: /bin/bash executable: /bin/bash
register: output register: output
changed_when: output.stdout != '' changed_when: output.stdout != ""
- setup: # gather facts - setup: # gather facts
tasks: tasks:
- name: install dependencies - name: install dependencies
apt: apt:
pkg: pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
- 'nginx'
- 'docker-compose'
- 'docker.io'
- 'certbot'
- 'python-certbot-nginx'
- name: request initial letsencrypt certificate - name: request initial letsencrypt certificate
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
@ -36,54 +26,24 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: file: path={{item.path}} state=directory
path: '{{item.path}}'
owner: '{{item.owner}}'
state: directory
with_items: with_items:
- path: '{{lemmy_base_dir}}/lemmy/' - { path: '/lemmy/' }
owner: 'root' - { path: '/lemmy/volumes/' }
- path: '{{lemmy_base_dir}}/volumes/'
owner: 'root'
- path: '{{lemmy_base_dir}}/volumes/pictrs/'
owner: '991'
- block: - block:
- name: add template files - name: add template files
template: template: src={{item.src}} dest={{item.dest}} mode={{item.mode}}
src: '{{item.src}}'
dest: '{{item.dest}}'
mode: '{{item.mode}}'
with_items: with_items:
- src: 'templates/docker-compose.yml' - { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' }
dest: '{{lemmy_base_dir}}/docker-compose.yml' - { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' }
mode: '0600' - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' }
- src: 'templates/nginx.conf'
dest: '/etc/nginx/sites-enabled/lemmy.conf'
mode: '0644'
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy: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: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
src: 'templates/config.hjson' vars:
dest: '{{lemmy_base_dir}}/lemmy.hjson' postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
mode: '0600' jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
force: false
owner: '1000'
group: '1000'
vars:
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
- name: build the dev docker image - name: build the dev docker image
local_action: shell cd .. && sudo docker build . -f docker/dev/Dockerfile -t lemmy:dev local_action: shell cd .. && sudo docker build . -f docker/dev/Dockerfile -t lemmy:dev
@ -98,29 +58,22 @@
local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar
- name: copy dev docker image to server - name: copy dev docker image to server
copy: copy: src=lemmy-dev.tar dest=/lemmy/lemmy-dev.tar
src: lemmy-dev.tar
dest: '{{lemmy_base_dir}}/lemmy-dev.tar'
- name: import docker image - name: import docker image
docker_image: docker_image:
name: lemmy name: lemmy
tag: dev tag: dev
load_path: '{{lemmy_base_dir}}/lemmy-dev.tar' load_path: /lemmy/lemmy-dev.tar
source: load source: load
force_source: yes force_source: yes
register: image_import register: image_import
- name: delete remote image file - name: delete remote image file
file: file: path=/lemmy/lemmy-dev.tar state=absent
path: '{{lemmy_base_dir}}/lemmy-dev.tar'
state: absent
- name: delete local image file - name: delete local image file
local_action: local_action: file path=lemmy-dev.tar state=absent
module: file
path: lemmy-dev.tar
state: absent
- name: enable and start docker service - name: enable and start docker service
systemd: systemd:
@ -132,10 +85,9 @@
# be a problem for testing # be a problem for testing
- name: start docker-compose - name: start docker-compose
docker_compose: docker_compose:
project_src: '{{lemmy_base_dir}}' project_src: /lemmy/
state: present state: present
recreate: always recreate: always
remove_orphans: yes
ignore_errors: yes ignore_errors: yes
- name: reload nginx with new config - name: reload nginx with new config
@ -143,7 +95,7 @@
- name: certbot renewal cronjob - name: certbot renewal cronjob
cron: cron:
special_time: daily special_time=daily
name: certbot-renew-lemmy name=certbot-renew-lemmy
user: root user=root
job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"

View file

@ -1,37 +1,14 @@
{ {
# for more info about the config, check out the documentation
# https://join.lemmy.ml/docs/en/administration/configuration.html
# settings related to the postgresql database
database: { database: {
# password to connect to postgres
password: "{{ postgres_password }}" password: "{{ postgres_password }}"
# host where postgres is running
host: "postgres" host: "postgres"
} }
# the domain name of your instance (eg "lemmy.ml")
hostname: "{{ domain }}" hostname: "{{ domain }}"
# json web token for authorization between server and client
jwt_secret: "{{ jwt_password }}" jwt_secret: "{{ jwt_password }}"
# email sending configuration front_end_dir: "/app/dist"
email: { email: {
# hostname of the smtp server
smtp_server: "postfix:25" smtp_server: "postfix:25"
# address to send emails from, eg "noreply@your-instance.com"
smtp_from_address: "noreply@{{ domain }}" smtp_from_address: "noreply@{{ domain }}"
use_tls: false use_tls: false
} }
# settings related to activitypub federation
federation: {
# whether to enable activitypub federation.
enabled: false
# Allows and blocks are described here:
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist
#
# comma separated list of instances with which federation is allowed
# Only one of these blocks should be uncommented
# allowed_instances: ["instance1.tld","instance2.tld"]
# comma separated list of instances which are blocked from federating
# blocked_instances: []
}
} }

View file

@ -1,4 +1,4 @@
version: '2.2' version: '3.3'
services: services:
lemmy: lemmy:
@ -12,21 +12,9 @@ services:
- ./lemmy.hjson:/config/config.hjson:ro - ./lemmy.hjson:/config/config.hjson:ro
depends_on: depends_on:
- postgres - postgres
- pictrs - pictshare
- iframely - iframely
lemmy-ui:
image: {{ lemmy_docker_ui_image }}
ports:
- "127.0.0.1:1235:1234"
restart: always
environment:
- LEMMY_INTERNAL_HOST=lemmy:8536
- LEMMY_EXTERNAL_HOST={{ domain }}
- LEMMY_HTTPS=true
depends_on:
- lemmy
postgres: postgres:
image: postgres:12-alpine image: postgres:12-alpine
environment: environment:
@ -37,15 +25,13 @@ services:
- ./volumes/postgres:/var/lib/postgresql/data - ./volumes/postgres:/var/lib/postgresql/data
restart: always restart: always
pictrs: pictshare:
image: asonix/pictrs:v0.2.6-r1 image: shtripok/pictshare:latest
user: 991:991
ports: ports:
- "127.0.0.1:8537:8080" - "127.0.0.1:8537:80"
volumes: volumes:
- ./volumes/pictrs:/mnt - ./volumes/pictshare:/usr/share/nginx/html/data
restart: always restart: always
mem_limit: 200m
iframely: iframely:
image: dogbin/iframely:latest image: dogbin/iframely:latest
@ -54,7 +40,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

@ -1,4 +1,4 @@
limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s; proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
server { server {
listen 80; listen 80;
@ -36,7 +36,7 @@ server {
# It might be nice to compress JSON, but leaving that out to protect against potential # It might be nice to compress JSON, but leaving that out to protect against potential
# compression+encryption information leak attacks like BREACH. # compression+encryption information leak attacks like BREACH.
gzip on; gzip on;
gzip_types text/css application/javascript image/svg+xml; gzip_types text/css application/javascript;
gzip_vary on; gzip_vary on;
# Only connect to this site via HTTPS for the two years # Only connect to this site via HTTPS for the two years
@ -48,55 +48,37 @@ server {
add_header X-Frame-Options "DENY"; add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
# Upload limit for pictrs # Upload limit for pictshare
client_max_body_size 20M; client_max_body_size 50M;
# frontend
location / { location / {
# The default ports: proxy_pass http://0.0.0.0:8536;
# lemmy_ui_port: 1235 proxy_set_header X-Real-IP $remote_addr;
# lemmy_port: 8536 proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
set $proxpass "http://0.0.0.0:{{ lemmy_ui_port }}"; # WebSocket support
if ($http_accept = "application/activity+json") { proxy_http_version 1.1;
set $proxpass "http://0.0.0.0:{{ lemmy_port }}"; proxy_set_header Upgrade $http_upgrade;
} proxy_set_header Connection "upgrade";
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) {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
}
proxy_pass $proxpass;
rewrite ^(.+)/+$ $1 permanent; # Proxy Cache
proxy_cache lemmy_frontend_cache;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
proxy_cache_lock on;
proxy_cache_min_uses 5;
}
# Send actual client IP upstream location /pictshare/ {
proxy_pass http://0.0.0.0:8537/;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# backend if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known) { add_header Cache-Control "public, max-age=31536000, immutable";
proxy_pass http://0.0.0.0:{{ lemmy_port }}; }
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Rate limit
limit_req zone=lemmy_ratelimit burst=30 nodelay;
# Add IP forwarding headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Redirect pictshare images to pictrs
location ~ /pictshare/(.*)$ {
return 301 /pictrs/image/$1;
} }
location /iframely/ { location /iframely/ {

26
ansible/uninstall.yml vendored
View file

@ -22,33 +22,27 @@
- name: stop docker-compose - name: stop docker-compose
docker_compose: docker_compose:
project_src: '{{lemmy_base_dir}}' project_src: /lemmy/
state: absent state: absent
- name: delete data - name: delete data
file: file: path={{item.path}} state=absent
path: '{{item.path}}'
state: absent
with_items: with_items:
- path: '{{lemmy_base_dir}}' - { path: '/lemmy/' }
- path: '/etc/nginx/sites-enabled/lemmy.conf' - { path: '/etc/nginx/sites-enabled/lemmy.conf' }
- name: Remove a volume - name: Remove a volume
docker_volume: docker_volume: name={{item.name}} state=absent
name: '{{item.name}}'
state: absent
with_items: with_items:
- name: 'lemmy_lemmy_db' - { name: 'lemmy_lemmy_db' }
- name: 'lemmy_lemmy_pictshare' - { name: 'lemmy_lemmy_pictshare' }
- name: delete entire ecloud folder - name: delete entire ecloud folder
file: file: path='/mnt/repo-base/' state=absent
path: '/mnt/repo-base/'
state: absent
when: delete_certs|bool when: delete_certs|bool
- name: remove certbot cronjob - name: remove certbot cronjob
cron: cron:
name: certbot-renew-lemmy name=certbot-renew-lemmy
state: absent state=absent

View file

@ -1,4 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

View file

@ -1,25 +0,0 @@
{
"name": "api_tests",
"version": "0.0.1",
"description": "API tests for lemmy backend",
"main": "index.js",
"repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines",
"license": "AGPL-3.0",
"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"
},
"devDependencies": {
"@types/jest": "^26.0.20",
"eslint": "^7.18.0",
"eslint-plugin-jane": "^9.0.3",
"jest": "^26.6.3",
"lemmy-js-client": "0.10.0-rc.4",
"node-fetch": "^2.6.1",
"prettier": "^2.1.2",
"ts-jest": "^26.4.4",
"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

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

View file

@ -1,166 +0,0 @@
jest.setTimeout(120000);
import {
alpha,
beta,
setupLogins,
searchForCommunity,
createCommunity,
deleteCommunity,
removeCommunity,
getCommunity,
followCommunity,
} from './shared';
import { CommunityView } from 'lemmy-js-client';
beforeAll(async () => {
await setupLogins();
});
function assertCommunityFederation(
communityOne: CommunityView,
communityTwo: CommunityView
) {
expect(communityOne.community.actor_id).toBe(communityTwo.community.actor_id);
expect(communityOne.community.name).toBe(communityTwo.community.name);
expect(communityOne.community.title).toBe(communityTwo.community.title);
expect(communityOne.community.description).toBe(
communityTwo.community.description
);
expect(communityOne.community.icon).toBe(communityTwo.community.icon);
expect(communityOne.community.banner).toBe(communityTwo.community.banner);
expect(communityOne.community.published).toBe(
communityTwo.community.published
);
expect(communityOne.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 () => {
let communityRes = await createCommunity(alpha);
expect(communityRes.community_view.community.name).toBeDefined();
// A dupe check
let prevName = communityRes.community_view.community.name;
let communityRes2: any = await createCommunity(alpha, prevName);
expect(communityRes2['error']).toBe('community_already_exists');
// Cache the community on beta, make sure it has the other fields
let searchShort = `!${prevName}@lemmy-alpha:8541`;
let search = await searchForCommunity(beta, searchShort);
let communityOnBeta = search.communities[0];
assertCommunityFederation(communityOnBeta, communityRes.community_view);
});
test('Delete community', async () => {
let communityRes = await createCommunity(beta);
// Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
// Follow the community from alpha
let follow = await followCommunity(
alpha,
true,
communityOnAlpha.community.id
);
// Make sure the follow response went through
expect(follow.community_view.community.local).toBe(false);
let deleteCommunityRes = await deleteCommunity(
beta,
true,
communityRes.community_view.community.id
);
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
// Make sure it got deleted on A
let communityOnAlphaDeleted = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true);
// Undelete
let undeleteCommunityRes = await deleteCommunity(
beta,
false,
communityRes.community_view.community.id
);
expect(undeleteCommunityRes.community_view.community.deleted).toBe(false);
// Make sure it got undeleted on A
let communityOnAlphaUnDeleted = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe(
false
);
});
test('Remove community', async () => {
let communityRes = await createCommunity(beta);
// Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
// Follow the community from alpha
let follow = await followCommunity(
alpha,
true,
communityOnAlpha.community.id
);
// Make sure the follow response went through
expect(follow.community_view.community.local).toBe(false);
let removeCommunityRes = await removeCommunity(
beta,
true,
communityRes.community_view.community.id
);
expect(removeCommunityRes.community_view.community.removed).toBe(true);
// Make sure it got Removed on A
let communityOnAlphaRemoved = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true);
// unremove
let unremoveCommunityRes = await removeCommunity(
beta,
false,
communityRes.community_view.community.id
);
expect(unremoveCommunityRes.community_view.community.removed).toBe(false);
// Make sure it got undeleted on A
let communityOnAlphaUnRemoved = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe(
false
);
});
test('Search for beta community', async () => {
let communityRes = await createCommunity(beta);
expect(communityRes.community_view.community.name).toBeDefined();
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
});

View file

@ -1,45 +0,0 @@
jest.setTimeout(120000);
import {
alpha,
setupLogins,
searchForBetaCommunity,
followCommunity,
checkFollowedCommunities,
unfollowRemotes,
} from './shared';
beforeAll(async () => {
await setupLogins();
});
afterAll(async () => {
await unfollowRemotes(alpha);
});
test('Follow federated community', async () => {
let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
let follow = await followCommunity(
alpha,
true,
search.communities[0].community.id
);
// Make sure the follow response went through
expect(follow.community_view.community.local).toBe(false);
expect(follow.community_view.community.name).toBe('main');
// Check it from local
let followCheck = await checkFollowedCommunities(alpha);
let remoteCommunityId = followCheck.communities.find(
c => c.community.local == false
).community.id;
expect(remoteCommunityId).toBeDefined();
// Test an unfollow
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
expect(unfollow.community_view.community.local).toBe(false);
// Make sure you are unsubbed locally
let unfollowCheck = await checkFollowedCommunities(alpha);
expect(unfollowCheck.communities.length).toBeGreaterThanOrEqual(1);
});

View file

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

@ -1,90 +0,0 @@
jest.setTimeout(120000);
import {
alpha,
beta,
setupLogins,
followBeta,
createPrivateMessage,
editPrivateMessage,
listPrivateMessages,
deletePrivateMessage,
unfollowRemotes,
} from './shared';
let recipient_id: number;
beforeAll(async () => {
await setupLogins();
let follow = await followBeta(alpha);
recipient_id = follow.community_view.creator.id;
});
afterAll(async () => {
await unfollowRemotes(alpha);
});
test('Create a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
expect(pmRes.private_message_view.private_message.content).toBeDefined();
expect(pmRes.private_message_view.private_message.local).toBe(true);
expect(pmRes.private_message_view.creator.local).toBe(true);
expect(pmRes.private_message_view.recipient.local).toBe(false);
let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBeDefined();
expect(betaPms.private_messages[0].private_message.local).toBe(false);
expect(betaPms.private_messages[0].creator.local).toBe(false);
expect(betaPms.private_messages[0].recipient.local).toBe(true);
});
test('Update a private message', async () => {
let updatedContent = 'A jest test federated private message edited';
let pmRes = await createPrivateMessage(alpha, recipient_id);
let pmUpdated = await editPrivateMessage(
alpha,
pmRes.private_message_view.private_message.id
);
expect(pmUpdated.private_message_view.private_message.content).toBe(
updatedContent
);
let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBe(
updatedContent
);
});
test('Delete a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await listPrivateMessages(beta);
let deletedPmRes = await deletePrivateMessage(
alpha,
true,
pmRes.private_message_view.private_message.id
);
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
// The GetPrivateMessages filters out deleted,
// even though they are in the actual database.
// no reason to show them
let betaPms2 = await listPrivateMessages(beta);
expect(betaPms2.private_messages.length).toBe(
betaPms1.private_messages.length - 1
);
// Undelete
let undeletedPmRes = await deletePrivateMessage(
alpha,
false,
pmRes.private_message_view.private_message.id
);
expect(undeletedPmRes.private_message_view.private_message.deleted).toBe(
false
);
let betaPms3 = await listPrivateMessages(beta);
expect(betaPms3.private_messages.length).toBe(
betaPms1.private_messages.length
);
});

View file

@ -1,636 +0,0 @@
import {
Login,
LoginResponse,
CreatePost,
EditPost,
CreateComment,
DeletePost,
RemovePost,
StickyPost,
LockPost,
PostResponse,
SearchResponse,
FollowCommunity,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostResponse,
Register,
Comment,
EditComment,
DeleteComment,
RemoveComment,
Search,
CommentResponse,
GetCommunity,
CreateCommunity,
DeleteCommunity,
RemoveCommunity,
GetPersonMentions,
CreateCommentLike,
CreatePostLike,
EditPrivateMessage,
DeletePrivateMessage,
GetFollowedCommunities,
GetPrivateMessages,
GetSite,
GetPost,
PrivateMessageResponse,
PrivateMessagesResponse,
GetPersonMentionsResponse,
SaveUserSettings,
SortType,
ListingType,
GetSiteResponse,
SearchType,
LemmyHttp,
BanPersonResponse,
BanPerson,
BanFromCommunity,
BanFromCommunityResponse,
Post,
CreatePrivateMessage,
} from 'lemmy-js-client';
export interface API {
client: LemmyHttp;
auth?: string;
}
export let alpha: API = {
client: new LemmyHttp('http://localhost:8541/api/v2'),
};
export let beta: API = {
client: new LemmyHttp('http://localhost:8551/api/v2'),
};
export let gamma: API = {
client: new LemmyHttp('http://localhost:8561/api/v2'),
};
export let delta: API = {
client: new LemmyHttp('http://localhost:8571/api/v2'),
};
export let epsilon: API = {
client: new LemmyHttp('http://localhost:8581/api/v2'),
};
export async function setupLogins() {
let formAlpha: Login = {
username_or_email: 'lemmy_alpha',
password: 'lemmy',
};
let resAlpha = alpha.client.login(formAlpha);
let formBeta = {
username_or_email: 'lemmy_beta',
password: 'lemmy',
};
let resBeta = beta.client.login(formBeta);
let formGamma = {
username_or_email: 'lemmy_gamma',
password: 'lemmy',
};
let resGamma = gamma.client.login(formGamma);
let formDelta = {
username_or_email: 'lemmy_delta',
password: 'lemmy',
};
let resDelta = delta.client.login(formDelta);
let formEpsilon = {
username_or_email: 'lemmy_epsilon',
password: 'lemmy',
};
let resEpsilon = epsilon.client.login(formEpsilon);
let res = await Promise.all([
resAlpha,
resBeta,
resGamma,
resDelta,
resEpsilon,
]);
alpha.auth = res[0].jwt;
beta.auth = res[1].jwt;
gamma.auth = res[2].jwt;
delta.auth = res[3].jwt;
epsilon.auth = res[4].jwt;
}
export async function createPost(
api: API,
community_id: number
): Promise<PostResponse> {
let name = randomString(5);
let body = randomString(10);
let url = 'https://google.com/';
let form: CreatePost = {
name,
url,
body,
auth: api.auth,
community_id,
nsfw: false,
};
return api.client.createPost(form);
}
export async function editPost(api: API, post: Post): Promise<PostResponse> {
let name = 'A jest test federated post, updated';
let form: EditPost = {
name,
post_id: post.id,
auth: api.auth,
nsfw: false,
};
return api.client.editPost(form);
}
export async function deletePost(
api: API,
deleted: boolean,
post: Post
): Promise<PostResponse> {
let form: DeletePost = {
post_id: post.id,
deleted: deleted,
auth: api.auth,
};
return api.client.deletePost(form);
}
export async function removePost(
api: API,
removed: boolean,
post: Post
): Promise<PostResponse> {
let form: RemovePost = {
post_id: post.id,
removed,
auth: api.auth,
};
return api.client.removePost(form);
}
export async function stickyPost(
api: API,
stickied: boolean,
post: Post
): Promise<PostResponse> {
let form: StickyPost = {
post_id: post.id,
stickied,
auth: api.auth,
};
return api.client.stickyPost(form);
}
export async function lockPost(
api: API,
locked: boolean,
post: Post
): Promise<PostResponse> {
let form: LockPost = {
post_id: post.id,
locked,
auth: api.auth,
};
return api.client.lockPost(form);
}
export async function searchPost(
api: API,
post: Post
): Promise<SearchResponse> {
let form: Search = {
q: post.ap_id,
type_: SearchType.Posts,
sort: SortType.TopAll,
};
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(
api: API,
post_id: number
): Promise<GetPostResponse> {
let form: GetPost = {
id: post_id,
};
return api.client.getPost(form);
}
export async function searchComment(
api: API,
comment: Comment
): Promise<SearchResponse> {
let form: Search = {
q: comment.ap_id,
type_: SearchType.Comments,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForBetaCommunity(
api: API
): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: Search = {
q: '!main@lemmy-beta:8551',
type_: SearchType.Communities,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForCommunity(
api: API,
q: string
): Promise<SearchResponse> {
// Use short-hand search url
let form: Search = {
q,
type_: SearchType.Communities,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForUser(
api: API,
apShortname: string
): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: Search = {
q: apShortname,
type_: SearchType.Users,
sort: SortType.TopAll,
};
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(
api: API,
follow: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: FollowCommunity = {
community_id,
follow,
auth: api.auth,
};
return api.client.followCommunity(form);
}
export async function checkFollowedCommunities(
api: API
): Promise<GetFollowedCommunitiesResponse> {
let form: GetFollowedCommunities = {
auth: api.auth,
};
return api.client.getFollowedCommunities(form);
}
export async function likePost(
api: API,
score: number,
post: Post
): Promise<PostResponse> {
let form: CreatePostLike = {
post_id: post.id,
score: score,
auth: api.auth,
};
return api.client.likePost(form);
}
export async function createComment(
api: API,
post_id: number,
parent_id?: number,
content = 'a jest test comment'
): Promise<CommentResponse> {
let form: CreateComment = {
content,
post_id,
parent_id,
auth: api.auth,
};
return api.client.createComment(form);
}
export async function editComment(
api: API,
comment_id: number,
content = 'A jest test federated comment update'
): Promise<CommentResponse> {
let form: EditComment = {
content,
comment_id,
auth: api.auth,
};
return api.client.editComment(form);
}
export async function deleteComment(
api: API,
deleted: boolean,
comment_id: number
): Promise<CommentResponse> {
let form: DeleteComment = {
comment_id,
deleted,
auth: api.auth,
};
return api.client.deleteComment(form);
}
export async function removeComment(
api: API,
removed: boolean,
comment_id: number
): Promise<CommentResponse> {
let form: RemoveComment = {
comment_id,
removed,
auth: api.auth,
};
return api.client.removeComment(form);
}
export async function getMentions(api: API): Promise<GetPersonMentionsResponse> {
let form: GetPersonMentions = {
sort: SortType.New,
unread_only: false,
auth: api.auth,
};
return api.client.getPersonMentions(form);
}
export async function likeComment(
api: API,
score: number,
comment: Comment
): Promise<CommentResponse> {
let form: CreateCommentLike = {
comment_id: comment.id,
score,
auth: api.auth,
};
return api.client.likeComment(form);
}
export async function createCommunity(
api: API,
name_: string = randomString(5)
): Promise<CommunityResponse> {
let description = 'a sample description';
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 form: CreateCommunity = {
name: name_,
title: name_,
description,
icon,
banner,
nsfw: false,
auth: api.auth,
};
return api.client.createCommunity(form);
}
export async function getCommunity(
api: API,
id: number
): Promise<CommunityResponse> {
let form: GetCommunity = {
id,
};
return api.client.getCommunity(form);
}
export async function deleteCommunity(
api: API,
deleted: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: DeleteCommunity = {
community_id,
deleted,
auth: api.auth,
};
return api.client.deleteCommunity(form);
}
export async function removeCommunity(
api: API,
removed: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: RemoveCommunity = {
community_id,
removed,
auth: api.auth,
};
return api.client.removeCommunity(form);
}
export async function createPrivateMessage(
api: API,
recipient_id: number
): Promise<PrivateMessageResponse> {
let content = 'A jest test federated private message';
let form: CreatePrivateMessage = {
content,
recipient_id,
auth: api.auth,
};
return api.client.createPrivateMessage(form);
}
export async function editPrivateMessage(
api: API,
private_message_id: number
): Promise<PrivateMessageResponse> {
let updatedContent = 'A jest test federated private message edited';
let form: EditPrivateMessage = {
content: updatedContent,
private_message_id,
auth: api.auth,
};
return api.client.editPrivateMessage(form);
}
export async function deletePrivateMessage(
api: API,
deleted: boolean,
private_message_id: number
): Promise<PrivateMessageResponse> {
let form: DeletePrivateMessage = {
deleted,
private_message_id,
auth: api.auth,
};
return api.client.deletePrivateMessage(form);
}
export async function registerUser(
api: API,
username: string = randomString(5)
): Promise<LoginResponse> {
let form: Register = {
username,
password: 'test',
password_verify: 'test',
show_nsfw: true,
};
return api.client.register(form);
}
export async function saveUserSettingsBio(
api: API,
auth: string
): Promise<LoginResponse> {
let form: SaveUserSettings = {
show_nsfw: true,
theme: 'darkly',
default_sort_type: Object.keys(SortType).indexOf(SortType.Active),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
lang: 'en',
show_avatars: true,
send_notifications_to_email: false,
bio: 'a changed bio',
auth,
};
return saveUserSettings(api, form);
}
export async function saveUserSettings(
api: API,
form: SaveUserSettings
): Promise<LoginResponse> {
return api.client.saveUserSettings(form);
}
export async function getSite(
api: API,
auth: string
): Promise<GetSiteResponse> {
let form: GetSite = {
auth,
};
return api.client.getSite(form);
}
export async function listPrivateMessages(
api: API
): Promise<PrivateMessagesResponse> {
let form: GetPrivateMessages = {
auth: api.auth,
unread_only: false,
limit: 999,
};
return api.client.getPrivateMessages(form);
}
export async function unfollowRemotes(
api: API
): Promise<GetFollowedCommunitiesResponse> {
// Unfollow all remote communities
let followed = await checkFollowedCommunities(api);
let remoteFollowed = followed.communities.filter(
c => c.community.local == false
);
for (let cu of remoteFollowed) {
await followCommunity(api, false, cu.community.id);
}
let followed2 = await checkFollowedCommunities(api);
return followed2;
}
export async function followBeta(api: API): Promise<CommunityResponse> {
// Cache it
let search = await searchForBetaCommunity(api);
let com = search.communities.find(c => c.community.local == false);
if (com) {
let follow = await followCommunity(api, true, com.community.id);
return follow;
}
}
export function delay(millis: number = 500) {
return new Promise(resolve => setTimeout(resolve, millis));
}
export function longDelay() {
return delay(10000);
}
export function wrapper(form: any): string {
return JSON.stringify(form);
}
function randomString(length: number): string {
var result = '';
var characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View file

@ -1,65 +0,0 @@
jest.setTimeout(120000);
import {
alpha,
beta,
registerUser,
searchForUser,
saveUserSettings,
getSite,
} from './shared';
import {
PersonViewSafe,
SaveUserSettings,
SortType,
ListingType,
} from 'lemmy-js-client';
let auth: string;
let apShortname: string;
function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) {
expect(userOne.person.name).toBe(userTwo.person.name);
expect(userOne.person.preferred_username).toBe(userTwo.person.preferred_username);
expect(userOne.person.bio).toBe(userTwo.person.bio);
expect(userOne.person.actor_id).toBe(userTwo.person.actor_id);
expect(userOne.person.avatar).toBe(userTwo.person.avatar);
expect(userOne.person.banner).toBe(userTwo.person.banner);
expect(userOne.person.published).toBe(userTwo.person.published);
}
test('Create user', async () => {
let userRes = await registerUser(alpha);
expect(userRes.jwt).toBeDefined();
auth = userRes.jwt;
let site = await getSite(alpha, auth);
expect(site.my_user).toBeDefined();
apShortname = `@${site.my_user.person.name}@lemmy-alpha:8541`;
});
test('Set some user settings, check that they are federated', async () => {
let avatar = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png';
let bio = 'a changed bio';
let form: SaveUserSettings = {
show_nsfw: false,
theme: '',
default_sort_type: Object.keys(SortType).indexOf(SortType.Hot),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
lang: '',
avatar,
banner,
preferred_username: 'user321',
show_avatars: false,
send_notifications_to_email: false,
bio,
auth,
};
await saveUserSettings(alpha, form);
let searchAlpha = await searchForUser(alpha, apShortname);
let userOnAlpha = searchAlpha.users[0];
let searchBeta = await searchForUser(beta, apShortname);
let userOnBeta = searchBeta.users[0];
assertUserFederation(userOnAlpha, userOnBeta);
});

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"]
}

4951
api_tests/yarn.lock vendored

File diff suppressed because it is too large Load diff

50
crates/api/Cargo.toml vendored
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)
}
}

View file

@ -1,903 +0,0 @@
use crate::{
check_community_ban,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_admin,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use anyhow::Context;
use lemmy_api_structs::{blocking, community::*};
use lemmy_apub::{
generate_apub_endpoint,
generate_followers_url,
generate_inbox_url,
generate_shared_inbox_url,
ActorType,
EndpointType,
};
use lemmy_db_queries::{
diesel_option_overwrite_to_url,
source::{
comment::Comment_,
community::{CommunityModerator_, Community_},
post::Post_,
},
ApubObject,
Bannable,
Crud,
Followable,
Joinable,
ListingType,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
PersonId,
};
use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
community_view::{CommunityQueryBuilder, CommunityView},
person_view::PersonViewSafe,
};
use lemmy_utils::{
apub::generate_actor_keypair,
location_info,
utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for GetCommunity {
type Response = GetCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &GetCommunity = &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 community_id = match data.id {
Some(id) => id,
None => {
let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
match blocking(context.pool(), move |conn| {
Community::read_from_name(conn, &name)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
}
.id
}
};
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 moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
.chat_server()
.send(GetCommunityUsersOnline { community_id })
.await
.unwrap_or(1);
let res = GetCommunityResponse {
community_view,
moderators,
online,
};
// Return the jwt
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &CreateCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs(&data.title)?;
check_slurs_opt(&data.description)?;
if !is_valid_community_name(&data.name) {
return Err(ApiError::err("invalid_community_name").into());
}
// Double check for duplicate community actor_ids
let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?;
let actor_id_cloned = community_actor_id.to_owned();
let community_dupe = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &actor_id_cloned)
})
.await?;
if community_dupe.is_ok() {
return Err(ApiError::err("community_already_exists").into());
}
// Check to make sure the icon and banners are urls
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
// When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?;
let community_form = CommunityForm {
name: data.name.to_owned(),
title: data.title.to_owned(),
description: data.description.to_owned(),
icon,
banner,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
updated: None,
actor_id: Some(community_actor_id.to_owned()),
local: true,
private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: None,
published: None,
followers_url: Some(generate_followers_url(&community_actor_id)?),
inbox_url: Some(generate_inbox_url(&community_actor_id)?),
shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)),
};
let inserted_community = match blocking(context.pool(), move |conn| {
Community::create(conn, &community_form)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("community_already_exists").into()),
};
// The community creator becomes a moderator
let community_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
person_id: local_user_view.person.id,
};
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(ApiError::err("community_moderator_already_exists").into());
}
// Follow your own community
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: local_user_view.person.id,
pending: false,
};
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(ApiError::err("community_follower_already_exists").into());
}
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, inserted_community.id, Some(person_id))
})
.await??;
Ok(CommunityResponse { community_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.title)?;
check_slurs_opt(&data.description)?;
// Verify its a mod (only mods can edit it)
let community_id = data.community_id;
let mods: Vec<PersonId> = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.moderator.id).collect())
})
.await??;
if !mods.contains(&local_user_view.person.id) {
return Err(ApiError::err("not_a_moderator").into());
}
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let community_form = CommunityForm {
name: read_community.name,
title: data.title.to_owned(),
description: data.description.to_owned(),
icon,
banner,
creator_id: read_community.creator_id,
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
nsfw: data.nsfw,
updated: Some(naive_now()),
actor_id: Some(read_community.actor_id),
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let community_id = data.community_id;
match blocking(context.pool(), move |conn| {
Community::update(conn, community_id, &community_form)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// TODO there needs to be some kind of an apub update
// process for communities and users
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeleteCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Verify its the creator (only a creator can delete the community)
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
if read_community.creator_id != local_user_view.person.id {
return Err(ApiError::err("no_community_edit_allowed").into());
}
// Do the delete
let community_id = data.community_id;
let deleted = data.deleted;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Send apub messages
if deleted {
updated_community.send_delete(context).await?;
} else {
updated_community.send_undo_delete(context).await?;
}
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemoveCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Verify its an admin (only an admin can remove a community)
is_admin(&local_user_view)?;
// Do the remove
let community_id = data.community_id;
let removed = data.removed;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_removed(conn, community_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Mod tables
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_person_id: local_user_view.person.id,
community_id: data.community_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(context.pool(), move |conn| {
ModRemoveCommunity::create(conn, &form)
})
.await??;
// Apub messages
if removed {
updated_community.send_remove(context).await?;
} else {
updated_community.send_undo_remove(context).await?;
}
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for ListCommunities {
type Response = ListCommunitiesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = &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,
};
// Don't show NSFW by default
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 communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.listing_type(&type_)
.sort(&sort)
.show_nsfw(show_nsfw)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
// Return the jwt
Ok(ListCommunitiesResponse { communities })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for FollowCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &FollowCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
person_id: local_user_view.person.id,
pending: false,
};
if community.local {
if data.follow {
check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(ApiError::err("community_follower_already_exists").into());
}
} else {
let unfollow =
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(ApiError::err("community_follower_already_exists").into());
}
}
} else if data.follow {
// Dont actually add to the community followers here, because you need
// to wait for the accept
local_user_view
.person
.send_follow(&community.actor_id(), context)
.await?;
} else {
local_user_view
.person
.send_unfollow(&community.actor_id(), context)
.await?;
let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(ApiError::err("community_follower_already_exists").into());
}
}
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
// TODO: this needs to return a "pending" state, until Accept is received from the remote server
// For now, just assume that remote follows are accepted.
// Otherwise, the subscribed will be null
if !community.local {
community_view.subscribed = data.follow;
}
Ok(CommunityResponse { community_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetFollowedCommunities {
type Response = GetFollowedCommunitiesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
let data: &GetFollowedCommunities = &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 communities = match blocking(context.pool(), move |conn| {
CommunityFollowerView::for_person(conn, person_id)
})
.await?
{
Ok(communities) => communities,
_ => return Err(ApiError::err("system_err_login").into()),
};
// Return the jwt
Ok(GetFollowedCommunitiesResponse { communities })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for BanFromCommunity {
type Response = BanFromCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<BanFromCommunityResponse, LemmyError> {
let data: &BanFromCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let banned_person_id = data.person_id;
// Verify that only mods or admins can ban
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
let community_user_ban_form = CommunityPersonBanForm {
community_id: data.community_id,
person_id: data.person_id,
};
if data.ban {
let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
if blocking(context.pool(), ban).await?.is_err() {
return Err(ApiError::err("community_user_already_banned").into());
}
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
person_id: banned_person_id,
pending: false,
};
blocking(context.pool(), move |conn: &'_ _| {
CommunityFollower::unfollow(conn, &community_follower_form)
})
.await?
.ok();
} else {
let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
if blocking(context.pool(), unban).await?.is_err() {
return Err(ApiError::err("community_user_already_banned").into());
}
}
// Remove/Restore their data if that's desired
if data.remove_data {
// Posts
blocking(context.pool(), move |conn: &'_ _| {
Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
})
.await??;
// Comments
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.creator_id(banned_person_id)
.community_id(community_id)
.limit(std::i64::MAX)
.list()
})
.await??;
for comment_view in &comments {
let comment_id = comment_view.comment.id;
blocking(context.pool(), move |conn: &'_ _| {
Comment::update_removed(conn, comment_id, true)
})
.await??;
}
}
// Mod tables
// TODO eventually do correct expires
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModBanFromCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
reason: data.reason.to_owned(),
banned: Some(data.ban),
expires,
};
blocking(context.pool(), move |conn| {
ModBanFromCommunity::create(conn, &form)
})
.await??;
let person_id = data.person_id;
let person_view = blocking(context.pool(), move |conn| {
PersonViewSafe::read(conn, person_id)
})
.await??;
let res = BanFromCommunityResponse {
person_view,
banned: data.ban,
};
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::BanFromCommunity,
response: res.clone(),
community_id,
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for AddModToCommunity {
type Response = AddModToCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<AddModToCommunityResponse, LemmyError> {
let data: &AddModToCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_moderator_form = CommunityModeratorForm {
community_id: data.community_id,
person_id: data.person_id,
};
let community_id = data.community_id;
// Verify that only mods or admins can add mod
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(ApiError::err("community_moderator_already_exists").into());
}
} else {
let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
if blocking(context.pool(), leave).await?.is_err() {
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(!data.added),
};
blocking(context.pool(), move |conn| {
ModAddCommunity::create(conn, &form)
})
.await??;
let community_id = data.community_id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let res = AddModToCommunityResponse { moderators };
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::AddModToCommunity,
response: res.clone(),
community_id,
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for TransferCommunity {
type Response = GetCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &TransferCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let site_creator_id = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.creator_id)
})
.await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
// Making sure the creator, if an admin, is at the top
let creator_index = admins
.iter()
.position(|r| r.person.id == site_creator_id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
// Make sure user is the creator, or an admin
if local_user_view.person.id != read_community.creator_id
&& !admins
.iter()
.map(|a| a.person.id)
.any(|x| x == local_user_view.person.id)
{
return Err(ApiError::err("not_an_admin").into());
}
let community_id = data.community_id;
let new_creator = data.person_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(context.pool(), update).await?.is_err() {
return Err(ApiError::err("couldnt_update_community").into());
};
// You also have to re-do the community_moderator table, reordering it.
let community_id = data.community_id;
let mut community_mods = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let creator_index = community_mods
.iter()
.position(|r| r.moderator.id == data.person_id)
.context(location_info!())?;
let creator_person = community_mods.remove(creator_index);
community_mods.insert(0, creator_person);
let community_id = data.community_id;
blocking(context.pool(), move |conn| {
CommunityModerator::delete_for_community(conn, community_id)
})
.await??;
// TODO: this should probably be a bulk operation
for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm {
community_id: cmod.community.id,
person_id: cmod.moderator.id,
};
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(false),
};
blocking(context.pool(), move |conn| {
ModAddCommunity::create(conn, &form)
})
.await??;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let community_id = data.community_id;
let moderators = match blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
// Return the jwt
Ok(GetCommunityResponse {
community_view,
moderators,
online: 0,
})
}
}
fn send_community_websocket(
res: &CommunityResponse,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
op: UserOperation,
) {
// Strip out the person id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community_view.subscribed = false;
context.chat_server().do_send(SendCommunityRoomMessage {
op,
response: res_sent,
community_id: res.community_view.community.id,
websocket_id,
});
}

View file

@ -1,586 +0,0 @@
use actix_web::{web, web::Data};
use lemmy_api_structs::{
blocking,
comment::*,
community::*,
person::*,
post::*,
site::*,
websocket::*,
};
use lemmy_db_queries::{
source::{
community::{CommunityModerator_, Community_},
site::Site_,
},
Crud,
DbPool,
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator},
post::Post,
site::Site,
},
CommunityId,
LocalUserId,
PersonId,
PostId,
};
use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
use lemmy_db_views_actor::{
community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView,
};
use lemmy_utils::{
claims::Claims,
settings::structs::Settings,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize;
use std::{env, process::Command};
use url::Url;
pub mod comment;
pub mod community;
pub mod local_user;
pub mod post;
pub mod routes;
pub mod site;
pub mod websocket;
#[async_trait::async_trait(?Send)]
pub trait Perform {
type Response: serde::ser::Serialize + Send;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError>;
}
pub(crate) async fn is_mod_or_admin(
pool: &DbPool,
person_id: PersonId,
community_id: CommunityId,
) -> Result<(), LemmyError> {
let is_mod_or_admin = blocking(pool, move |conn| {
CommunityView::is_mod_or_admin(conn, person_id, community_id)
})
.await?;
if !is_mod_or_admin {
return Err(ApiError::err("not_a_mod_or_admin").into());
}
Ok(())
}
pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
if !local_user_view.local_user.admin {
return Err(ApiError::err("not_an_admin").into());
}
Ok(())
}
pub(crate) async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
Ok(post) => Ok(post),
Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
}
}
pub(crate) async fn get_local_user_view_from_jwt(
jwt: &str,
pool: &DbPool,
) -> Result<LocalUserView, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let local_user_id = LocalUserId(claims.sub);
let local_user_view =
blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
// Check for a site ban
if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into());
}
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view)
}
/// Checks if user's token was issued before user's password reset.
pub(crate) fn check_validator_time(
validator_time: &chrono::NaiveDateTime,
claims: &Claims,
) -> Result<(), LemmyError> {
let user_validation_time = validator_time.timestamp();
if user_validation_time > claims.iat {
Err(ApiError::err("not_logged_in").into())
} else {
Ok(())
}
}
pub(crate) async fn get_local_user_view_from_jwt_opt(
jwt: &Option<String>,
pool: &DbPool,
) -> Result<Option<LocalUserView>, LemmyError> {
match jwt {
Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
None => Ok(None),
}
}
pub(crate) async fn get_local_user_settings_view_from_jwt(
jwt: &str,
pool: &DbPool,
) -> Result<LocalUserSettingsView, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let local_user_id = LocalUserId(claims.sub);
let local_user_view = blocking(pool, move |conn| {
LocalUserSettingsView::read(conn, local_user_id)
})
.await??;
// Check for a site ban
if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into());
}
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view)
}
pub(crate) async fn get_local_user_settings_view_from_jwt_opt(
jwt: &Option<String>,
pool: &DbPool,
) -> Result<Option<LocalUserSettingsView>, LemmyError> {
match jwt {
Some(jwt) => Ok(Some(
get_local_user_settings_view_from_jwt(jwt, pool).await?,
)),
None => Ok(None),
}
}
pub(crate) async fn check_community_ban(
person_id: PersonId,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
let is_banned =
move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
Err(ApiError::err("community_ban").into())
} else {
Ok(())
}
}
pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
if score == -1 {
let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
if !site.enable_downvotes {
return Err(ApiError::err("downvotes_disabled").into());
}
}
Ok(())
}
/// Returns a list of communities that the user moderates
/// or if a community_id is supplied validates the user is a moderator
/// of that community and returns the community id in a vec
///
/// * `person_id` - the person id of the moderator
/// * `community_id` - optional community id to check for moderator privileges
/// * `pool` - the diesel db pool
pub(crate) async fn collect_moderated_communities(
person_id: PersonId,
community_id: Option<CommunityId>,
pool: &DbPool,
) -> Result<Vec<CommunityId>, LemmyError> {
if let Some(community_id) = community_id {
// if the user provides a community_id, just check for mod/admin privileges
is_mod_or_admin(pool, person_id, community_id).await?;
Ok(vec![community_id])
} else {
let ids = blocking(pool, move |conn: &'_ _| {
CommunityModerator::get_person_moderated_communities(conn, person_id)
})
.await??;
Ok(ids)
}
}
pub(crate) async fn build_federated_instances(
pool: &DbPool,
) -> Result<Option<FederatedInstances>, LemmyError> {
if Settings::get().federation().enabled {
let distinct_communities = blocking(pool, move |conn| {
Community::distinct_federated_communities(conn)
})
.await??;
let allowed = Settings::get().get_allowed_instances();
let blocked = Settings::get().get_blocked_instances();
let mut linked = distinct_communities
.iter()
.map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
.collect::<Result<Vec<String>, LemmyError>>()?;
if let Some(allowed) = allowed.as_ref() {
linked.extend_from_slice(allowed);
}
if let Some(blocked) = blocked.as_ref() {
linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
}
// Sort and remove dupes
linked.sort_unstable();
linked.dedup();
Ok(Some(FederatedInstances {
linked,
allowed,
blocked,
}))
} else {
Ok(None)
}
}
pub async fn match_websocket_operation(
context: LemmyContext,
id: ConnectionId,
op: UserOperation,
data: &str,
) -> Result<String, LemmyError> {
match op {
// User ops
UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
UserOperation::GetPersonDetails => {
do_websocket_operation::<GetPersonDetails>(context, id, op, data).await
}
UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
UserOperation::GetPersonMentions => {
do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
}
UserOperation::MarkPersonMentionAsRead => {
do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
}
UserOperation::MarkAllAsRead => {
do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
}
UserOperation::DeleteAccount => {
do_websocket_operation::<DeleteAccount>(context, id, op, data).await
}
UserOperation::PasswordReset => {
do_websocket_operation::<PasswordReset>(context, id, op, data).await
}
UserOperation::PasswordChange => {
do_websocket_operation::<PasswordChange>(context, id, op, data).await
}
UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
UserOperation::CommunityJoin => {
do_websocket_operation::<CommunityJoin>(context, id, op, data).await
}
UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
UserOperation::SaveUserSettings => {
do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
}
UserOperation::GetReportCount => {
do_websocket_operation::<GetReportCount>(context, id, op, data).await
}
// Private Message ops
UserOperation::CreatePrivateMessage => {
do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
}
UserOperation::EditPrivateMessage => {
do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
}
UserOperation::DeletePrivateMessage => {
do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
}
UserOperation::MarkPrivateMessageAsRead => {
do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
}
UserOperation::GetPrivateMessages => {
do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
}
// Site ops
UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
UserOperation::GetSiteConfig => {
do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
}
UserOperation::SaveSiteConfig => {
do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
}
UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
UserOperation::TransferCommunity => {
do_websocket_operation::<TransferCommunity>(context, id, op, data).await
}
UserOperation::TransferSite => {
do_websocket_operation::<TransferSite>(context, id, op, data).await
}
// Community ops
UserOperation::GetCommunity => {
do_websocket_operation::<GetCommunity>(context, id, op, data).await
}
UserOperation::ListCommunities => {
do_websocket_operation::<ListCommunities>(context, id, op, data).await
}
UserOperation::CreateCommunity => {
do_websocket_operation::<CreateCommunity>(context, id, op, data).await
}
UserOperation::EditCommunity => {
do_websocket_operation::<EditCommunity>(context, id, op, data).await
}
UserOperation::DeleteCommunity => {
do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
}
UserOperation::RemoveCommunity => {
do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
}
UserOperation::FollowCommunity => {
do_websocket_operation::<FollowCommunity>(context, id, op, data).await
}
UserOperation::GetFollowedCommunities => {
do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
}
UserOperation::BanFromCommunity => {
do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
}
UserOperation::AddModToCommunity => {
do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
}
// Post ops
UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
UserOperation::CreatePostLike => {
do_websocket_operation::<CreatePostLike>(context, id, op, data).await
}
UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
UserOperation::CreatePostReport => {
do_websocket_operation::<CreatePostReport>(context, id, op, data).await
}
UserOperation::ListPostReports => {
do_websocket_operation::<ListPostReports>(context, id, op, data).await
}
UserOperation::ResolvePostReport => {
do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
}
// Comment ops
UserOperation::CreateComment => {
do_websocket_operation::<CreateComment>(context, id, op, data).await
}
UserOperation::EditComment => {
do_websocket_operation::<EditComment>(context, id, op, data).await
}
UserOperation::DeleteComment => {
do_websocket_operation::<DeleteComment>(context, id, op, data).await
}
UserOperation::RemoveComment => {
do_websocket_operation::<RemoveComment>(context, id, op, data).await
}
UserOperation::MarkCommentAsRead => {
do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
}
UserOperation::SaveComment => {
do_websocket_operation::<SaveComment>(context, id, op, data).await
}
UserOperation::GetComments => {
do_websocket_operation::<GetComments>(context, id, op, data).await
}
UserOperation::CreateCommentLike => {
do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
}
UserOperation::CreateCommentReport => {
do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
}
UserOperation::ListCommentReports => {
do_websocket_operation::<ListCommentReports>(context, id, op, data).await
}
UserOperation::ResolveCommentReport => {
do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
}
}
}
async fn do_websocket_operation<'a, 'b, Data>(
context: LemmyContext,
id: ConnectionId,
op: UserOperation,
data: &str,
) -> Result<String, LemmyError>
where
for<'de> Data: Deserialize<'de> + 'a,
Data: Perform,
{
let parsed_data: Data = serde_json::from_str(&data)?;
let res = parsed_data
.perform(&web::Data::new(context), Some(id))
.await?;
serialize_websocket_message(&op, &res)
}
pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
let mut built_text = String::new();
// Building proper speech text for espeak
for mut c in captcha.chars() {
let new_str = if c.is_alphabetic() {
if c.is_lowercase() {
c.make_ascii_uppercase();
format!("lower case {} ... ", c)
} else {
c.make_ascii_uppercase();
format!("capital {} ... ", c)
}
} else {
format!("{} ...", c)
};
built_text.push_str(&new_str);
}
espeak_wav_base64(&built_text)
}
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
// Make a temp file path
let uuid = uuid::Uuid::new_v4().to_string();
let file_path = format!(
"{}/lemmy_espeak_{}.wav",
env::temp_dir().to_string_lossy(),
&uuid
);
// Write the wav file
Command::new("espeak")
.arg("-w")
.arg(&file_path)
.arg(text)
.status()?;
// Read the wav file bytes
let bytes = std::fs::read(&file_path)?;
// Delete the file
std::fs::remove_file(file_path)?;
// Convert to base64
let base64 = base64::encode(bytes);
Ok(base64)
}
/// Checks the password length
pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
if pass.len() > 60 {
Err(ApiError::err("invalid_password").into())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{captcha_espeak_wav_base64, check_validator_time};
use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud};
use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserForm},
person::{Person, PersonForm},
};
use lemmy_utils::claims::Claims;
#[test]
fn test_should_not_validate_user_token_after_password_change() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "Gerry9812".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 local_user_form = LocalUserForm {
person_id: inserted_person.id,
email: None,
matrix_user_id: None,
password_encrypted: "123456".to_string(),
admin: None,
show_nsfw: None,
theme: None,
default_sort_type: None,
default_listing_type: None,
lang: None,
show_avatars: None,
send_notifications_to_email: None,
};
let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
let jwt = Claims::jwt(inserted_local_user.id.0).unwrap();
let claims = Claims::decode(&jwt).unwrap().claims;
let check = check_validator_time(&inserted_local_user.validator_time, &claims);
assert!(check.is_ok());
// The check should fail, since the validator time is now newer than the jwt issue time
let updated_local_user =
LocalUser::update_password(&conn, inserted_local_user.id, &"password111").unwrap();
let check_after = check_validator_time(&updated_local_user.validator_time, &claims);
assert!(check_after.is_err());
let num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, num_deleted);
}
#[test]
fn test_espeak() {
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
}
}

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,598 +0,0 @@
use crate::{
build_federated_instances,
get_local_user_settings_view_from_jwt,
get_local_user_settings_view_from_jwt_opt,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_admin,
Perform,
};
use actix_web::web::Data;
use anyhow::Context;
use lemmy_api_structs::{blocking, person::Register, site::*};
use lemmy_apub::fetcher::search::search_by_apub_id;
use lemmy_db_queries::{
diesel_option_overwrite_to_url,
source::site::Site_,
Crud,
SearchType,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{
moderator::*,
site::{Site, *},
},
};
use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_view::PostQueryBuilder,
site_view::SiteView,
};
use lemmy_db_views_actor::{
community_view::CommunityQueryBuilder,
person_view::{PersonQueryBuilder, PersonViewSafe},
};
use lemmy_db_views_moderator::{
mod_add_community_view::ModAddCommunityView,
mod_add_view::ModAddView,
mod_ban_from_community_view::ModBanFromCommunityView,
mod_ban_view::ModBanView,
mod_lock_post_view::ModLockPostView,
mod_remove_comment_view::ModRemoveCommentView,
mod_remove_community_view::ModRemoveCommunityView,
mod_remove_post_view::ModRemovePostView,
mod_sticky_post_view::ModStickyPostView,
};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt},
version,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetUsersOnline, SendAllMessage},
LemmyContext,
UserOperation,
};
use log::{debug, info};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for GetModlog {
type Response = GetModlogResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = &self;
let community_id = data.community_id;
let mod_person_id = data.mod_person_id;
let page = data.page;
let limit = data.limit;
let removed_posts = blocking(context.pool(), move |conn| {
ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let locked_posts = blocking(context.pool(), move |conn| {
ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let stickied_posts = blocking(context.pool(), move |conn| {
ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let removed_comments = blocking(context.pool(), move |conn| {
ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let banned_from_community = blocking(context.pool(), move |conn| {
ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let added_to_community = blocking(context.pool(), move |conn| {
ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
// These arrays are only for the full modlog, when a community isn't given
let (removed_communities, banned, added) = if data.community_id.is_none() {
blocking(context.pool(), move |conn| {
Ok((
ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
ModBanView::list(conn, mod_person_id, page, limit)?,
ModAddView::list(conn, mod_person_id, page, limit)?,
)) as Result<_, LemmyError>
})
.await??
} else {
(Vec::new(), Vec::new(), Vec::new())
};
// Return the jwt
Ok(GetModlogResponse {
removed_posts,
locked_posts,
stickied_posts,
removed_comments,
removed_communities,
banned_from_community,
banned,
added_to_community,
added,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateSite {
type Response = SiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<SiteResponse, LemmyError> {
let data: &CreateSite = &self;
let read_site = move |conn: &'_ _| Site::read_simple(conn);
if blocking(context.pool(), read_site).await?.is_ok() {
return Err(ApiError::err("site_already_exists").into());
};
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.description)?;
// Make sure user is an admin
is_admin(&local_user_view)?;
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
icon: Some(data.icon.to_owned().map(|url| url.into())),
banner: Some(data.banner.to_owned().map(|url| url.into())),
creator_id: local_user_view.person.id,
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
updated: None,
};
let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
if blocking(context.pool(), create_site).await?.is_err() {
return Err(ApiError::err("site_already_exists").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
Ok(SiteResponse { site_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditSite {
type Response = SiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.description)?;
// Make sure user is an admin
is_admin(&local_user_view)?;
let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
icon,
banner,
creator_id: found_site.creator_id,
updated: Some(naive_now()),
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
};
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
if blocking(context.pool(), update_site).await?.is_err() {
return Err(ApiError::err("couldnt_update_site").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
let res = SiteResponse { site_view };
context.chat_server().do_send(SendAllMessage {
op: UserOperation::EditSite,
response: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetSite {
type Response = GetSiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &GetSite = &self;
let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
Ok(site_view) => Some(site_view),
// If the site isn't created yet, check the setup
Err(_) => {
if let Some(setup) = Settings::get().setup().as_ref() {
let register = Register {
username: setup.admin_username.to_owned(),
email: setup.admin_email.to_owned(),
password: setup.admin_password.to_owned(),
password_verify: setup.admin_password.to_owned(),
show_nsfw: true,
captcha_uuid: None,
captcha_answer: None,
};
let login_response = register.perform(context, websocket_id).await?;
info!("Admin {} created", setup.admin_username);
let create_site = CreateSite {
name: setup.site_name.to_owned(),
description: None,
icon: None,
banner: None,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
auth: login_response.jwt,
};
create_site.perform(context, websocket_id).await?;
info!("Site {} created", setup.site_name);
Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
} else {
None
}
}
};
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
// Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() {
let site_creator_id = site_view.creator.id;
// TODO investigate why this is sometimes coming back null
// Maybe user_.admin isn't being set to true?
if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
}
}
let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
let online = context
.chat_server()
.send(GetUsersOnline)
.await
.unwrap_or(1);
let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?;
let federated_instances = build_federated_instances(context.pool()).await?;
Ok(GetSiteResponse {
site_view,
admins,
banned,
online,
version: version::VERSION.to_string(),
my_user,
federated_instances,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Search {
type Response = SearchResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<SearchResponse, LemmyError> {
let data: &Search = &self;
match search_by_apub_id(&data.q, context).await {
Ok(r) => return Ok(r),
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
}
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_ = SearchType::from_str(&data.type_)?;
let mut posts = Vec::new();
let mut comments = Vec::new();
let mut communities = Vec::new();
let mut users = Vec::new();
// TODO no clean / non-nsfw searching rn
let q = data.q.to_owned();
let page = data.page;
let limit = data.limit;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
match type_ {
SearchType::Posts => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Comments => {
comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(&conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Communities => {
communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Users => {
users = blocking(context.pool(), move |conn| {
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::All => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
users = blocking(context.pool(), move |conn| {
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Url => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.my_person_id(person_id)
.community_id(community_id)
.community_name(community_name)
.url_search(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
};
// Return the jwt
Ok(SearchResponse {
type_: data.type_.to_owned(),
comments,
posts,
communities,
users,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for TransferSite {
type Response = GetSiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
is_admin(&local_user_view)?;
let read_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
// Make sure user is the creator
if read_site.creator_id != local_user_view.person.id {
return Err(ApiError::err("not_an_admin").into());
}
let new_creator_id = data.person_id;
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
if blocking(context.pool(), transfer_site).await?.is_err() {
return Err(ApiError::err("couldnt_update_site").into());
};
// Mod tables
let form = ModAddForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
removed: Some(false),
};
blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
.position(|r| r.person.id == site_view.creator.id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
let federated_instances = build_federated_instances(context.pool()).await?;
let my_user = Some(get_local_user_settings_view_from_jwt(&data.auth, context.pool()).await?);
Ok(GetSiteResponse {
site_view: Some(site_view),
admins,
banned,
online: 0,
version: version::VERSION.to_string(),
my_user,
federated_instances,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetSiteConfig {
type Response = GetSiteConfigResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &GetSiteConfig = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
is_admin(&local_user_view)?;
let config_hjson = Settings::read_config_file()?;
Ok(GetSiteConfigResponse { config_hjson })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SaveSiteConfig {
type Response = GetSiteConfigResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &SaveSiteConfig = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
is_admin(&local_user_view)?;
// Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
let config_hjson = match Settings::save_config_file(&data.config_hjson) {
Ok(config_hjson) => config_hjson,
Err(_e) => return Err(ApiError::err("couldnt_update_site").into()),
};
Ok(GetSiteConfigResponse { config_hjson })
}
}

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,133 +0,0 @@
use lemmy_db_schema::{CommunityId, PersonId};
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct GetCommunity {
pub id: Option<CommunityId>,
pub name: Option<String>,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetCommunityResponse {
pub community_view: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
pub online: usize,
}
#[derive(Deserialize)]
pub struct CreateCommunity {
pub name: String,
pub title: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct CommunityResponse {
pub community_view: CommunityView,
}
#[derive(Deserialize, Debug)]
pub struct ListCommunities {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct ListCommunitiesResponse {
pub communities: Vec<CommunityView>,
}
#[derive(Deserialize, Clone)]
pub struct BanFromCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct BanFromCommunityResponse {
pub person_view: PersonViewSafe,
pub banned: bool,
}
#[derive(Deserialize)]
pub struct AddModToCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct AddModToCommunityResponse {
pub moderators: Vec<CommunityModeratorView>,
}
#[derive(Deserialize)]
pub struct EditCommunity {
pub community_id: CommunityId,
pub title: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteCommunity {
pub community_id: CommunityId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveCommunity {
pub community_id: CommunityId,
pub removed: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct FollowCommunity {
pub community_id: CommunityId,
pub follow: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetFollowedCommunities {
pub auth: String,
}
#[derive(Serialize)]
pub struct GetFollowedCommunitiesResponse {
pub communities: Vec<CommunityFollowerView>,
}
#[derive(Deserialize)]
pub struct TransferCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub auth: String,
}

View file

@ -1,191 +0,0 @@
pub mod comment;
pub mod community;
pub mod person;
pub mod post;
pub mod site;
pub mod websocket;
use diesel::PgConnection;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::{
source::{
comment::Comment,
person::Person,
person_mention::{PersonMention, PersonMentionForm},
post::Post,
},
LocalUserId,
};
use lemmy_db_views::local_user_view::LocalUserView;
use lemmy_utils::{email::send_email, settings::structs::Settings, utils::MentionData, LemmyError};
use log::error;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerLink {
pub rel: Option<String>,
#[serde(rename(serialize = "type", deserialize = "type"))]
pub type_: Option<String>,
pub href: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerResponse {
pub subject: String,
pub aliases: Vec<Url>,
pub links: Vec<WebFingerLink>,
}
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub async fn send_local_notifs(
mentions: Vec<MentionData>,
comment: Comment,
person: Person,
post: Post,
pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<LocalUserId>, LemmyError> {
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email)
})
.await?;
Ok(ids)
}
fn do_send_local_notifs(
conn: &PgConnection,
mentions: &[MentionData],
comment: &Comment,
person: &Person,
post: &Post,
do_send_email: bool,
) -> Vec<LocalUserId> {
let mut recipient_ids = Vec::new();
// Send the local mentions
for mention in mentions
.iter()
.filter(|m| m.is_local() && m.name.ne(&person.name))
.collect::<Vec<&MentionData>>()
{
if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) {
// TODO
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionForm {
recipient_id: mention_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
PersonMention::create(&conn, &user_mention_form).ok();
// Send an email to those local users that have notifications on
if do_send_email {
send_email_to_user(
&mention_user_view,
"Mentioned by",
"Person Mention",
&comment.content,
)
}
}
}
// Send notifs to the parent commenter / poster
match comment.parent_id {
Some(parent_id) => {
if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
// Don't send a notif to yourself
if parent_comment.creator_id != person.id {
// Get the parent commenter local_user
if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id)
{
recipient_ids.push(parent_user_view.local_user.id);
if do_send_email {
send_email_to_user(
&parent_user_view,
"Reply from",
"Comment Reply",
&comment.content,
)
}
}
}
}
}
// Its a post
None => {
if post.creator_id != person.id {
if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) {
recipient_ids.push(parent_user_view.local_user.id);
if do_send_email {
send_email_to_user(
&parent_user_view,
"Reply from",
"Post Reply",
&comment.content,
)
}
}
}
}
};
recipient_ids
}
pub fn send_email_to_user(
local_user_view: &LocalUserView,
subject_text: &str,
body_text: &str,
comment_content: &str,
) {
if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
return;
}
if let Some(user_email) = &local_user_view.local_user.email {
let subject = &format!(
"{} - {} {}",
subject_text,
Settings::get().hostname(),
local_user_view.person.name,
);
let html = &format!(
"<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
body_text,
local_user_view.person.name,
comment_content,
Settings::get().get_protocol_and_hostname()
);
match send_email(subject, &user_email, &local_user_view.person.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}

View file

@ -1,245 +0,0 @@
use lemmy_db_views::{
comment_view::CommentView,
post_view::PostView,
private_message_view::PrivateMessageView,
};
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
person_mention_view::PersonMentionView,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct Login {
pub username_or_email: String,
pub password: String,
}
use lemmy_db_schema::{CommunityId, PersonId, PersonMentionId, PrivateMessageId};
#[derive(Deserialize)]
pub struct Register {
pub username: String,
pub email: Option<String>,
pub password: String,
pub password_verify: String,
pub show_nsfw: bool,
pub captcha_uuid: Option<String>,
pub captcha_answer: Option<String>,
}
#[derive(Deserialize)]
pub struct GetCaptcha {}
#[derive(Serialize)]
pub struct GetCaptchaResponse {
pub ok: Option<CaptchaResponse>, // Will be None if captchas are disabled
}
#[derive(Serialize)]
pub struct CaptchaResponse {
pub png: String, // A Base64 encoded png
pub wav: Option<String>, // A Base64 encoded wav audio
pub uuid: String,
}
#[derive(Deserialize)]
pub struct SaveUserSettings {
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 avatar: Option<String>,
pub banner: Option<String>,
pub preferred_username: Option<String>,
pub email: Option<String>,
pub bio: Option<String>,
pub matrix_user_id: Option<String>,
pub new_password: Option<String>,
pub new_password_verify: Option<String>,
pub old_password: Option<String>,
pub show_avatars: Option<bool>,
pub send_notifications_to_email: Option<bool>,
pub auth: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub jwt: String,
}
#[derive(Deserialize)]
pub struct GetPersonDetails {
pub person_id: Option<PersonId>,
pub username: Option<String>,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<CommunityId>,
pub saved_only: bool,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetPersonDetailsResponse {
pub person_view: PersonViewSafe,
pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>,
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
}
#[derive(Serialize)]
pub struct GetRepliesResponse {
pub replies: Vec<CommentView>,
}
#[derive(Serialize)]
pub struct GetPersonMentionsResponse {
pub mentions: Vec<PersonMentionView>,
}
#[derive(Deserialize)]
pub struct MarkAllAsRead {
pub auth: String,
}
#[derive(Deserialize)]
pub struct AddAdmin {
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct AddAdminResponse {
pub admins: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct BanPerson {
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct BanPersonResponse {
pub person_view: PersonViewSafe,
pub banned: bool,
}
#[derive(Deserialize)]
pub struct GetReplies {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetPersonMentions {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkPersonMentionAsRead {
pub person_mention_id: PersonMentionId,
pub read: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView,
}
#[derive(Deserialize)]
pub struct DeleteAccount {
pub password: String,
pub auth: String,
}
#[derive(Deserialize)]
pub struct PasswordReset {
pub email: String,
}
#[derive(Serialize, Clone)]
pub struct PasswordResetResponse {}
#[derive(Deserialize)]
pub struct PasswordChange {
pub token: String,
pub password: String,
pub password_verify: String,
}
#[derive(Deserialize)]
pub struct CreatePrivateMessage {
pub content: String,
pub recipient_id: PersonId,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPrivateMessage {
pub private_message_id: PrivateMessageId,
pub content: String,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeletePrivateMessage {
pub private_message_id: PrivateMessageId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkPrivateMessageAsRead {
pub private_message_id: PrivateMessageId,
pub read: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetPrivateMessages {
pub unread_only: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PrivateMessagesResponse {
pub private_messages: Vec<PrivateMessageView>,
}
#[derive(Serialize, Clone)]
pub struct PrivateMessageResponse {
pub private_message_view: PrivateMessageView,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetReportCount {
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GetReportCountResponse {
pub community: Option<CommunityId>,
pub comment_reports: i64,
pub post_reports: i64,
}

View file

@ -1,149 +0,0 @@
use lemmy_db_schema::{CommunityId, PostId};
use lemmy_db_views::{
comment_view::CommentView,
post_report_view::PostReportView,
post_view::PostView,
};
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Debug)]
pub struct CreatePost {
pub name: String,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub community_id: CommunityId,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PostResponse {
pub post_view: PostView,
}
#[derive(Deserialize)]
pub struct GetPost {
pub id: PostId,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetPostResponse {
pub post_view: PostView,
pub community_view: CommunityView,
pub comments: Vec<CommentView>,
pub moderators: Vec<CommunityModeratorView>,
pub online: usize,
}
#[derive(Deserialize, Debug)]
pub struct GetPosts {
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, Debug)]
pub struct GetPostsResponse {
pub posts: Vec<PostView>,
}
#[derive(Deserialize)]
pub struct CreatePostLike {
pub post_id: PostId,
pub score: i16,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPost {
pub post_id: PostId,
pub name: String,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeletePost {
pub post_id: PostId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemovePost {
pub post_id: PostId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct LockPost {
pub post_id: PostId,
pub locked: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct StickyPost {
pub post_id: PostId,
pub stickied: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SavePost {
pub post_id: PostId,
pub save: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct CreatePostReport {
pub post_id: PostId,
pub reason: String,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CreatePostReportResponse {
pub success: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResolvePostReport {
pub report_id: i32,
pub resolved: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ResolvePostReportResponse {
pub report_id: i32,
pub resolved: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListPostReports {
pub page: Option<i64>,
pub limit: Option<i64>,
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct ListPostReportsResponse {
pub posts: Vec<PostReportView>,
}

View file

@ -1,137 +0,0 @@
use lemmy_db_schema::{CommunityId, PersonId};
use lemmy_db_views::{
comment_view::CommentView,
local_user_view::LocalUserSettingsView,
post_view::PostView,
site_view::SiteView,
};
use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
use lemmy_db_views_moderator::{
mod_add_community_view::ModAddCommunityView,
mod_add_view::ModAddView,
mod_ban_from_community_view::ModBanFromCommunityView,
mod_ban_view::ModBanView,
mod_lock_post_view::ModLockPostView,
mod_remove_comment_view::ModRemoveCommentView,
mod_remove_community_view::ModRemoveCommunityView,
mod_remove_post_view::ModRemovePostView,
mod_sticky_post_view::ModStickyPostView,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Debug)]
pub struct Search {
pub q: String,
pub type_: String,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct SearchResponse {
pub type_: String,
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
pub communities: Vec<CommunityView>,
pub users: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct GetModlog {
pub mod_person_id: Option<PersonId>,
pub community_id: Option<CommunityId>,
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Serialize)]
pub struct GetModlogResponse {
pub removed_posts: Vec<ModRemovePostView>,
pub locked_posts: Vec<ModLockPostView>,
pub stickied_posts: Vec<ModStickyPostView>,
pub removed_comments: Vec<ModRemoveCommentView>,
pub removed_communities: Vec<ModRemoveCommunityView>,
pub banned_from_community: Vec<ModBanFromCommunityView>,
pub banned: Vec<ModBanView>,
pub added_to_community: Vec<ModAddCommunityView>,
pub added: Vec<ModAddView>,
}
#[derive(Deserialize)]
pub struct CreateSite {
pub name: String,
pub description: Option<String>,
pub icon: Option<Url>,
pub banner: Option<Url>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditSite {
pub name: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetSite {
pub auth: Option<String>,
}
#[derive(Serialize, Clone)]
pub struct SiteResponse {
pub site_view: SiteView,
}
#[derive(Serialize)]
pub struct GetSiteResponse {
pub site_view: Option<SiteView>, // Because the site might not be set up yet
pub admins: Vec<PersonViewSafe>,
pub banned: Vec<PersonViewSafe>,
pub online: usize,
pub version: String,
pub my_user: Option<LocalUserSettingsView>,
pub federated_instances: Option<FederatedInstances>, // Federation may be disabled
}
#[derive(Deserialize)]
pub struct TransferSite {
pub person_id: PersonId,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetSiteConfig {
pub auth: String,
}
#[derive(Serialize)]
pub struct GetSiteConfigResponse {
pub config_hjson: String,
}
#[derive(Deserialize)]
pub struct SaveSiteConfig {
pub config_hjson: String,
pub auth: String,
}
#[derive(Serialize)]
pub struct FederatedInstances {
pub linked: Vec<String>,
pub allowed: Option<Vec<String>>,
pub blocked: Option<Vec<String>>,
}

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,260 +0,0 @@
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt};
use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
base::ExtendsExt,
};
use anyhow::Context;
use lemmy_api_structs::{blocking, comment::CommentResponse, send_local_notifs};
use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
use lemmy_db_schema::source::{
comment::{Comment, CommentLike, CommentLikeForm},
post::Post,
};
use lemmy_db_views::comment_view::CommentView;
use lemmy_utils::{location_info, utils::scrape_text_for_mentions, LemmyError};
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
pub(crate) async fn receive_create_comment(
create: Create,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&create, context, request_counter).await?;
let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
// Note:
// Although mentions could be gotten from the post tags (they are included there), or the ccs,
// Its much easier to scrape them from the comment body, since the API has to do that
// anyway.
let mentions = scrape_text_for_mentions(&comment.content);
let recipient_ids = send_local_notifs(
mentions,
comment.clone(),
person,
post,
context.pool(),
true,
)
.await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment.id, None)
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_comment(
update: Update,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let person = get_actor_as_person(&update, context, request_counter).await?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?;
let comment_id = comment.id;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment.content);
let recipient_ids =
send_local_notifs(mentions, comment, person, post, context.pool(), false).await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_like_comment(
like: Like,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&like, context, request_counter).await?;
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
person_id: person.id,
score: 1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_dislike_comment(
dislike: Dislike,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&dislike, context, request_counter).await?;
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
person_id: person.id,
score: -1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let deleted_comment = blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment.id, true)
})
.await??;
// Refetch the view
let comment_id = deleted_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_comment(
context: &LemmyContext,
_remove: Remove,
comment: Comment,
) -> Result<(), LemmyError> {
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, true)
})
.await??;
// Refetch the view
let comment_id = removed_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}

View file

@ -1,150 +0,0 @@
use crate::activities::receive::get_actor_as_person;
use activitystreams::activity::{Dislike, Like};
use lemmy_api_structs::{blocking, comment::CommentResponse};
use lemmy_db_queries::{source::comment::Comment_, Likeable};
use lemmy_db_schema::source::comment::{Comment, CommentLike};
use lemmy_db_views::comment_view::CommentView;
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
pub(crate) async fn receive_undo_like_comment(
like: &Like,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(like, context, request_counter).await?;
let comment_id = comment.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_dislike_comment(
dislike: &Dislike,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(dislike, context, request_counter).await?;
let comment_id = comment.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let deleted_comment = blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment.id, false)
})
.await??;
// Refetch the view
let comment_id = deleted_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, false)
})
.await??;
// Refetch the view
let comment_id = removed_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}

View file

@ -1,167 +0,0 @@
use crate::{activities::receive::verify_activity_domains_valid, inbox::is_addressed_to_public};
use activitystreams::{
activity::{ActorAndObjectRefExt, Delete, Remove, Undo},
base::{AnyBase, ExtendsExt},
};
use anyhow::Context;
use lemmy_api_structs::{blocking, community::CommunityResponse};
use lemmy_db_queries::{source::community::Community_, ApubObject};
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
use url::Url;
pub(crate) async fn receive_delete_community(
context: &LemmyContext,
community: Community,
) -> Result<(), LemmyError> {
let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, true)
})
.await??;
let community_id = deleted_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, true)
})
.await??;
let community_id = removed_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_community(
context: &LemmyContext,
undo: Undo,
community: Community,
expected_domain: &Url,
) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
is_addressed_to_public(&delete)?;
let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, false)
})
.await??;
let community_id = deleted_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let remove = Remove::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, false)
})
.await??;
let community_id = removed_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}

View file

@ -1,81 +0,0 @@
use crate::fetcher::person::get_or_fetch_and_upsert_person;
use activitystreams::{
activity::{ActorAndObjectRef, ActorAndObjectRefExt},
base::{AsBase, BaseExt},
error::DomainError,
};
use anyhow::{anyhow, Context};
use lemmy_db_schema::source::person::Person;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use std::fmt::Debug;
use url::Url;
pub(crate) mod comment;
pub(crate) mod comment_undo;
pub(crate) mod community;
pub(crate) mod post;
pub(crate) mod post_undo;
pub(crate) mod private_message;
/// Return HTTP 501 for unsupported activities in inbox.
pub(crate) fn receive_unhandled_activity<A>(activity: A) -> Result<(), LemmyError>
where
A: Debug,
{
debug!("received unhandled activity type: {:?}", activity);
Err(anyhow!("Activity not supported").into())
}
/// Reads the actor field of an activity and returns the corresponding `Person`.
pub(crate) async fn get_actor_as_person<T, A>(
activity: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Person, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef,
{
let actor = activity.actor()?;
let person_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
get_or_fetch_and_upsert_person(&person_uri, context, request_counter).await
}
/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally
/// also checks the ID of the inner object.
///
/// The reason that this starts with the actor ID is that it was already confirmed as correct by the
/// HTTP signature.
pub(crate) fn verify_activity_domains_valid<T, Kind>(
activity: &T,
actor_id: &Url,
object_domain_must_match: bool,
) -> Result<(), LemmyError>
where
T: AsBase<Kind> + ActorAndObjectRef,
{
let expected_domain = actor_id.domain().context(location_info!())?;
activity.id(expected_domain)?;
let object_id = match activity.object().to_owned().single_xsd_any_uri() {
// object is just an ID
Some(id) => id,
// object is something like an activity, a comment or a post
None => activity
.object()
.to_owned()
.one()
.context(location_info!())?
.id()
.context(location_info!())?
.to_owned(),
};
if object_domain_must_match && object_id.domain() != Some(expected_domain) {
return Err(DomainError.into());
}
Ok(())
}

View file

@ -1,199 +0,0 @@
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, PageExt};
use activitystreams::{
activity::{Create, Dislike, Like, Remove, Update},
prelude::*,
};
use anyhow::Context;
use lemmy_api_structs::{blocking, post::PostResponse};
use lemmy_db_queries::{source::post::Post_, Likeable};
use lemmy_db_schema::source::post::{Post, PostLike, PostLikeForm};
use lemmy_db_views::post_view::PostView;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
pub(crate) async fn receive_create_post(
create: Create,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&create, context, request_counter).await?;
let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?;
// Refetch the view
let post_id = post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_post(
update: Update,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&update, context, request_counter).await?;
let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?;
let post_id = post.id;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_like_post(
like: Like,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&like, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
person_id: person.id,
score: 1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_dislike_post(
dislike: Dislike,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&dislike, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
person_id: person.id,
score: -1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let deleted_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post.id, true)
})
.await??;
// Refetch the view
let post_id = deleted_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_post(
context: &LemmyContext,
_remove: Remove,
post: Post,
) -> Result<(), LemmyError> {
let removed_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post.id, true)
})
.await??;
// Refetch the view
let post_id = removed_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}

View file

@ -1,125 +0,0 @@
use crate::activities::receive::get_actor_as_person;
use activitystreams::activity::{Dislike, Like};
use lemmy_api_structs::{blocking, post::PostResponse};
use lemmy_db_queries::{source::post::Post_, Likeable};
use lemmy_db_schema::source::post::{Post, PostLike};
use lemmy_db_views::post_view::PostView;
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
pub(crate) async fn receive_undo_like_post(
like: &Like,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(like, context, request_counter).await?;
let post_id = post.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_dislike_post(
dislike: &Dislike,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(dislike, context, request_counter).await?;
let post_id = post.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let deleted_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post.id, false)
})
.await??;
// Refetch the view
let post_id = deleted_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let removed_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post.id, false)
})
.await??;
// Refetch the view
let post_id = removed_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}

View file

@ -1,228 +0,0 @@
use crate::{
activities::receive::verify_activity_domains_valid,
check_is_apub_id_valid,
fetcher::person::get_or_fetch_and_upsert_person,
inbox::get_activity_to_and_cc,
objects::FromApub,
NoteExt,
};
use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
base::{AsBase, ExtendsExt},
object::AsObject,
public,
};
use anyhow::{anyhow, Context};
use lemmy_api_structs::{blocking, person::PrivateMessageResponse};
use lemmy_db_queries::source::private_message::PrivateMessage_;
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
use url::Url;
pub(crate) async fn receive_create_private_message(
context: &LemmyContext,
create: Create,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&create, context, request_counter).await?;
let note = NoteExt::from_any_base(
create
.object()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?;
let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message.id)
})
.await??;
let res = PrivateMessageResponse {
private_message_view: message,
};
// Send notifications to the local recipient, if one exists
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage,
response: res,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_private_message(
context: &LemmyContext,
update: Update,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&update, context, request_counter).await?;
let object = update
.object()
.as_one()
.context(location_info!())?
.to_owned();
let note = NoteExt::from_any_base(object)?.context(location_info!())?;
let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?;
let private_message_id = private_message.id;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
let res = PrivateMessageResponse {
private_message_view: message,
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_private_message(
context: &LemmyContext,
delete: Delete,
private_message: PrivateMessage,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&delete, context, request_counter).await?;
let deleted_private_message = blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, private_message.id, true)
})
.await??;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(&conn, deleted_private_message.id)
})
.await??;
let res = PrivateMessageResponse {
private_message_view: message,
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_private_message(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
private_message: PrivateMessage,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&undo, context, request_counter).await?;
let object = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
check_private_message_activity_valid(&delete, context, request_counter).await?;
let deleted_private_message = blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, private_message.id, false)
})
.await??;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(&conn, deleted_private_message.id)
})
.await??;
let res = PrivateMessageResponse {
private_message_view: message,
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
async fn check_private_message_activity_valid<T, Kind>(
activity: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.len() != 1 {
return Err(anyhow!("Private message can only be addressed to one person").into());
}
if to_and_cc.contains(&public()) {
return Err(anyhow!("Private message cant be public").into());
}
let person_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
check_is_apub_id_valid(&person_id)?;
// check that the sender is a person, not a community
get_or_fetch_and_upsert_person(&person_id, &context, request_counter).await?;
Ok(())
}

View file

@ -1,440 +0,0 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
base::AnyBase,
link::Mention,
prelude::*,
public,
};
use anyhow::anyhow;
use itertools::Itertools;
use lemmy_api_structs::{blocking, WebFingerResponse};
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
use lemmy_utils::{
request::{retry, RecvError},
settings::structs::Settings,
utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use log::debug;
use reqwest::Client;
use serde_json::Error;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community and
/// mentioned persons.
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(&self, &community, context).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(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(create.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, create, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community and mentioned
/// persons.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(&self, &community, context).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(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(update.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, update, context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(delete, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_delete(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
// Generate a fake delete activity, with the correct object
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(public())
.set_many_ccs(vec![community.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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
async fn send_remove(&self, mod_: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(remove, &mod_, &community, context).await?;
Ok(())
}
async fn send_undo_remove(
&self,
mod_: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
// Generate a fake delete activity, with the correct object
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
mod_.actor_id.to_owned().into_inner(),
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &mod_, &community, context).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_like(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
}
struct MentionsAndAddresses {
ccs: Vec<Url>,
inboxes: Vec<Url>,
tags: Vec<Mention>,
}
impl MentionsAndAddresses {
fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
self
.tags
.iter()
.map(|t| t.to_owned().into_any_base())
.collect::<Result<Vec<AnyBase>, Error>>()
}
}
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the persons / addresses that go in the cc field.
async fn collect_non_local_mentions(
comment: &Comment,
community: &Community,
context: &LemmyContext,
) -> Result<MentionsAndAddresses, LemmyError> {
let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()];
// Note: dont include community inbox here, as we send to it separately with `send_to_community()`
let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()];
// Add the mention tag
let mut tags = Vec::new();
// Get the person IDs for any mentions
let mentions = scrape_text_for_mentions(&comment.content)
.into_iter()
// Filter only the non-local ones
.filter(|m| !m.is_local())
.collect::<Vec<MentionData>>();
for mention in &mentions {
// TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?;
inboxes.push(mention_person.get_shared_inbox_or_inbox_url());
let mut mention_tag = Mention::new();
mention_tag.set_href(actor_id).set_name(mention.full_name());
tags.push(mention_tag);
}
}
let inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
ccs: addressed_ccs,
inboxes,
tags,
})
}
/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a
/// top-level comment, the creator of the post, otherwise the creator of the parent comment.
async fn get_comment_parent_creator(
pool: &DbPool,
comment: &Comment,
) -> Result<Person, LemmyError> {
let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id {
let parent_comment =
blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
parent_comment.creator_id
} else {
let parent_post_id = comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
parent_post.creator_id
};
Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??)
}
/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// using webfinger.
async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
Settings::get().get_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| anyhow!("No application/activity+json link found."))?;
link
.href
.to_owned()
.ok_or_else(|| anyhow!("No href found.").into())
}

View file

@ -1,214 +0,0 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
insert_activity,
ActorType,
};
use activitystreams::{
activity::{
kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
Accept,
ActorAndObjectRefExt,
Announce,
Delete,
Follow,
Remove,
Undo,
},
base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt,
public,
};
use anyhow::Context;
use itertools::Itertools;
use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Community {
fn is_local(&self) -> bool {
self.local
}
fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner()
}
fn public_key(&self) -> Option<String> {
self.public_key.to_owned()
}
fn private_key(&self) -> Option<String> {
self.private_key.to_owned()
}
fn get_shared_inbox_or_inbox_url(&self) -> Url {
self
.shared_inbox_url
.clone()
.unwrap_or_else(|| self.inbox_url.to_owned())
.into()
}
async fn send_follow(
&self,
_follow_actor_id: &Url,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_unfollow(
&self,
_follow_actor_id: &Url,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
/// As a local community, accept the follow request from a remote person.
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let actor_uri = follow
.actor()?
.as_single_xsd_any_uri()
.context(location_info!())?;
let person = get_or_fetch_and_upsert_person(actor_uri, context, &mut 0).await?;
let mut accept = Accept::new(
self.actor_id.to_owned().into_inner(),
follow.into_any_base()?,
);
accept
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AcceptType::Accept)?)
.set_to(person.actor_id());
send_activity_single_dest(accept, self, person.inbox_url.into(), context).await?;
Ok(())
}
/// If the creator of a community deletes the community, send this to all followers.
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(delete, self, context).await?;
Ok(())
}
/// If the creator of a community reverts the deletion of a community, send this to all followers.
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
}
/// If an admin removes a community, send this to all followers.
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(remove, self, context).await?;
Ok(())
}
/// If an admin reverts the removal of a community, send this to all followers.
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
// Undo that fake activity
let mut undo = Undo::new(self.actor_id(), remove.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
}
/// Wraps an activity sent to the community in an announce, and then sends the announce to all
/// community followers.
///
/// If we are announcing a local activity, it hasn't been stored in the database yet, and we need
/// to do it here, so that it can be fetched by ID. Remote activities are inserted into DB in the
/// inbox.
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let inner_id = activity.id().context(location_info!())?;
if inner_id.domain() == Some(&Settings::get().get_hostname_without_port()?) {
insert_activity(inner_id, activity.clone(), true, false, context.pool()).await?;
}
let mut announce = Announce::new(self.actor_id.to_owned().into_inner(), activity);
announce
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AnnounceType::Announce)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(announce, self, context).await?;
Ok(())
}
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
let id = self.id;
let follows = blocking(pool, move |conn| {
CommunityFollowerView::for_community(conn, id)
})
.await??;
let inboxes = follows
.into_iter()
.filter(|f| !f.follower.local)
.map(|f| f.follower.shared_inbox_url.unwrap_or(f.follower.inbox_url))
.map(|i| i.into_inner())
.unique()
// Don't send to blocked instances
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.collect();
Ok(inboxes)
}
}

View file

@ -1,24 +0,0 @@
use lemmy_utils::settings::structs::Settings;
use url::{ParseError, Url};
use uuid::Uuid;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

View file

@ -1,149 +0,0 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
ActorType,
};
use activitystreams::{
activity::{
kind::{FollowType, UndoType},
Follow,
Undo,
},
base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool, Followable};
use lemmy_db_schema::source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
person::Person,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Person {
fn is_local(&self) -> bool {
self.local
}
fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner()
}
fn public_key(&self) -> Option<String> {
self.public_key.to_owned()
}
fn private_key(&self) -> Option<String> {
self.private_key.to_owned()
}
fn get_shared_inbox_or_inbox_url(&self) -> Url {
self
.shared_inbox_url
.clone()
.unwrap_or_else(|| self.inbox_url.to_owned())
.into()
}
/// As a given local person, send out a follow request to a remote community.
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let follow_actor_id = follow_actor_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &follow_actor_id.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: self.id,
pending: true,
};
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(conn, &community_follower_form).ok()
})
.await?;
let mut follow = Follow::new(self.actor_id.to_owned().into_inner(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id());
send_activity_single_dest(follow, self, community.inbox_url.into(), context).await?;
Ok(())
}
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let follow_actor_id = follow_actor_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &follow_actor_id.into())
})
.await??;
let mut follow = Follow::new(self.actor_id.to_owned().into_inner(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id());
// Undo that fake activity
let mut undo = Undo::new(
self.actor_id.to_owned().into_inner(),
follow.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(community.actor_id());
send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
Ok(())
}
async fn send_accept_follow(
&self,
_follow: Follow,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_announce(
&self,
_activity: AnyBase,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
unimplemented!()
}
}

View file

@ -1,274 +0,0 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_to_community,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
prelude::*,
public,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(create, creator, &community, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(update, creator, &community, context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(delete, creator, &community, context).await?;
Ok(())
}
async fn send_undo_delete(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_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(public())
.set_many_ccs(vec![community.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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, creator, &community, context).await?;
Ok(())
}
async fn send_remove(&self, mod_: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(remove, mod_, &community, context).await?;
Ok(())
}
async fn send_undo_remove(
&self,
mod_: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
mod_.actor_id.to_owned().into_inner(),
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, mod_, &community, context).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_like(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
}

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,320 +0,0 @@
use crate::{
check_is_apub_id_valid,
extensions::signatures::sign_and_send,
insert_activity,
ActorType,
APUB_JSON_CONTENT_TYPE,
};
use activitystreams::{
base::{BaseExt, Extends, ExtendsExt},
object::AsObject,
};
use anyhow::{anyhow, Context, Error};
use background_jobs::{
create_server,
memory_storage::Storage,
ActixJob,
Backoff,
MaxRetries,
QueueHandle,
WorkerConfig,
};
use itertools::Itertools;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::{debug, warn};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
use url::Url;
/// Sends a local activity to a single, remote actor.
///
/// * `activity` the apub activity to be sent
/// * `creator` the local actor which created the activity
/// * `inbox` the inbox url where the activity should be delivered to
pub(crate) async fn send_activity_single_dest<T, Kind>(
activity: T,
creator: &dyn ActorType,
inbox: Url,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
if check_is_apub_id_valid(&inbox).is_ok() {
debug!(
"Sending activity {:?} to {}",
&activity.id_unchecked(),
&inbox
);
send_activity_internal(
context.activity_queue(),
activity,
creator,
vec![inbox],
context.pool(),
true,
true,
)
.await?;
}
Ok(())
}
/// From a local community, send activity to all remote followers.
///
/// * `activity` the apub activity to send
/// * `community` the sending community
/// * `sender_shared_inbox` in case of an announce, this should be the shared inbox of the inner
/// activities creator, as receiving a known activity will cause an error
pub(crate) async fn send_to_community_followers<T, Kind>(
activity: T,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let follower_inboxes: Vec<Url> = community
.get_follower_inboxes(context.pool())
.await?
.iter()
.unique()
.filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname()))
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.map(|inbox| inbox.to_owned())
.collect();
debug!(
"Sending activity {:?} to followers of {}",
&activity.id_unchecked().map(|i| i.to_string()),
&community.actor_id
);
send_activity_internal(
context.activity_queue(),
activity,
community,
follower_inboxes,
context.pool(),
true,
false,
)
.await?;
Ok(())
}
/// Sends an activity from a local person to a remote community.
///
/// * `activity` the activity to send
/// * `creator` the creator of the activity
/// * `community` the destination community
///
pub(crate) async fn send_to_community<T, Kind>(
activity: T,
creator: &Person,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
// if this is a local community, we need to do an announce from the community instead
if community.local {
community
.send_announce(activity.into_any_base()?, context)
.await?;
} else {
let inbox = community.get_shared_inbox_or_inbox_url();
check_is_apub_id_valid(&inbox)?;
debug!(
"Sending activity {:?} to community {}",
&activity.id_unchecked(),
&community.actor_id
);
send_activity_internal(
context.activity_queue(),
activity,
creator,
vec![inbox],
context.pool(),
true,
false,
)
.await?;
}
Ok(())
}
/// Sends notification to any persons mentioned in a comment
///
/// * `creator` person who created the comment
/// * `mentions` list of inboxes of persons which are mentioned in the comment
/// * `activity` either a `Create/Note` or `Update/Note`
pub(crate) async fn send_comment_mentions<T, Kind>(
creator: &Person,
mentions: Vec<Url>,
activity: T,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
debug!(
"Sending mentions activity {:?} to {:?}",
&activity.id_unchecked(),
&mentions
);
let mentions = mentions
.iter()
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.map(|i| i.to_owned())
.collect();
send_activity_internal(
context.activity_queue(),
activity,
creator,
mentions,
context.pool(),
false, // Don't create a new DB row
false,
)
.await?;
Ok(())
}
/// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as
/// handling signing and retrying failed deliveres.
///
/// The caller of this function needs to remove any blocked domains from `to`,
/// using `check_is_apub_id_valid()`.
async fn send_activity_internal<T, Kind>(
activity_sender: &QueueHandle,
activity: T,
actor: &dyn ActorType,
inboxes: Vec<Url>,
pool: &DbPool,
insert_into_db: bool,
sensitive: bool,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
if !Settings::get().federation().enabled || inboxes.is_empty() {
return Ok(());
}
// Don't send anything to ourselves
let hostname = Settings::get().get_hostname_without_port()?;
let inboxes: Vec<&Url> = inboxes
.iter()
.filter(|i| i.domain().expect("valid inbox url") != hostname)
.collect();
let activity = activity.into_any_base()?;
let serialised_activity = serde_json::to_string(&activity)?;
// This is necessary because send_comment and send_comment_mentions
// might send the same ap_id
if insert_into_db {
let id = activity.id().context(location_info!())?;
insert_activity(id, activity.clone(), true, sensitive, pool).await?;
}
for i in inboxes {
let message = SendActivityTask {
activity: serialised_activity.to_owned(),
inbox: i.to_owned(),
actor_id: actor.actor_id(),
private_key: actor.private_key().context(location_info!())?,
};
if env::var("LEMMY_TEST_SEND_SYNC").is_ok() {
do_send(message, &Client::default()).await?;
} else {
activity_sender.queue::<SendActivityTask>(message)?;
}
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct SendActivityTask {
activity: String,
inbox: Url,
actor_id: Url,
private_key: String,
}
/// Signs the activity with the sending actor's key, and delivers to the given inbox. Also retries
/// if the delivery failed.
impl ActixJob for SendActivityTask {
type State = MyState;
type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>;
const NAME: &'static str = "SendActivityTask";
const MAX_RETRIES: MaxRetries = MaxRetries::Count(10);
const BACKOFF: Backoff = Backoff::Exponential(2);
fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { do_send(self, &state.client).await })
}
}
async fn do_send(task: SendActivityTask, client: &Client) -> Result<(), Error> {
let mut headers = BTreeMap::<String, String>::new();
headers.insert("Content-Type".into(), APUB_JSON_CONTENT_TYPE.to_string());
let result = sign_and_send(
client,
headers,
&task.inbox,
task.activity.clone(),
&task.actor_id,
task.private_key.to_owned(),
)
.await;
if let Err(e) = result {
warn!("{}", e);
return Err(anyhow!(
"Failed to send activity {} to {}",
&task.activity,
task.inbox
));
}
Ok(())
}
pub fn create_activity_queue() -> QueueHandle {
// Start the application server. This guards access to to the jobs store
let queue_handle = create_server(Storage::new());
// Configure and start our workers
WorkerConfig::new(|| MyState {
client: Client::default(),
})
.register::<SendActivityTask>()
.start(queue_handle.clone());
queue_handle
}
#[derive(Clone)]
struct MyState {
pub client: Client,
}

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,38 +0,0 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Community field
/// `sensitive` (called 'nsfw' in Lemmy).
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupExtension {
pub sensitive: Option<bool>,
}
impl GroupExtension {
pub fn new(sensitive: bool) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension {
sensitive: Some(sensitive),
})
}
}
impl<U> UnparsedExtension<U> for GroupExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension {
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}
}

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,36 +0,0 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Post fields
/// `comemnts_enabled` (called 'locked' in Lemmy),
/// `sensitive` (called 'nsfw') and `stickied`.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageExtension {
pub comments_enabled: Option<bool>,
pub sensitive: Option<bool>,
pub stickied: Option<bool>,
}
impl<U> UnparsedExtension<U> for PageExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PageExtension {
comments_enabled: unparsed_mut.remove("commentsEnabled")?,
sensitive: unparsed_mut.remove("sensitive")?,
stickied: unparsed_mut.remove("stickied")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
unparsed_mut.insert("stickied", self.stickied)?;
Ok(())
}
}

View file

@ -1,139 +0,0 @@
use crate::ActorType;
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
use http::{header::HeaderName, HeaderMap, HeaderValue};
use http_signature_normalization_actix::Config as ConfigActix;
use http_signature_normalization_reqwest::prelude::{Config, SignExt};
use lemmy_utils::{location_info, LemmyError};
use log::debug;
use openssl::{
hash::MessageDigest,
pkey::PKey,
sign::{Signer, Verifier},
};
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, str::FromStr};
use url::Url;
lazy_static! {
static ref CONFIG2: ConfigActix = ConfigActix::new();
static ref HTTP_SIG_CONFIG: Config = Config::new();
}
/// Creates an HTTP post request to `inbox_url`, with the given `client` and `headers`, and
/// `activity` as request body. The request is signed with `private_key` and then sent.
pub async fn sign_and_send(
client: &Client,
headers: BTreeMap<String, String>,
inbox_url: &Url,
activity: String,
actor_id: &Url,
private_key: String,
) -> Result<Response, LemmyError> {
let signing_key_id = format!("{}#main-key", actor_id);
let mut header_map = HeaderMap::new();
for h in headers {
header_map.insert(
HeaderName::from_str(h.0.as_str())?,
HeaderValue::from_str(h.1.as_str())?,
);
}
let response = client
.post(&inbox_url.to_string())
.headers(header_map)
.signature_with_digest(
HTTP_SIG_CONFIG.clone(),
signing_key_id,
Sha256::new(),
activity,
move |signing_string| {
let private_key = PKey::private_key_from_pem(private_key.as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
signer.update(signing_string.as_bytes())?;
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError>
},
)
.await?;
Ok(response)
}
/// Verifies the HTTP signature on an incoming inbox request.
pub(crate) fn verify_signature(
request: &HttpRequest,
actor: &dyn ActorType,
) -> Result<(), LemmyError> {
let public_key = actor.public_key().context(location_info!())?;
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
request.headers().clone(),
)?
.verify(|signature, signing_string| -> Result<bool, LemmyError> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(&signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()).into())
}
}
/// Extension for actor public key, which is needed on person and community for HTTP signatures.
///
/// Taken from: https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub id: String,
pub owner: Url,
pub public_key_pem: String,
}
impl PublicKey {
pub fn to_ext(&self) -> PublicKeyExtension {
PublicKeyExtension {
public_key: self.to_owned(),
}
}
}
impl<U> UnparsedExtension<U> for PublicKeyExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(PublicKeyExtension {
public_key: unparsed_mut.remove("publicKey")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("publicKey", self.public_key)?;
Ok(())
}
}

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,37 +0,0 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
};
use actix_web::{body::Body, web, web::Path, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::{source::comment::Comment, CommentId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommentQuery {
comment_id: String,
}
/// Return the ActivityPub json representation of a local comment over HTTP.
pub async fn get_apub_comment(
info: Path<CommentQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let id = CommentId(info.comment_id.parse::<i32>()?);
let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
if !comment.local {
return Err(NotFound.into());
}
if !comment.deleted {
Ok(create_apub_response(
&comment.to_apub(context.pool()).await?,
))
} else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
}
}

View file

@ -1,113 +0,0 @@
use crate::{
extensions::context::lemmy_context,
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
ActorType,
};
use activitystreams::{
base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::{activity::Activity_, community::Community_};
use lemmy_db_schema::source::{activity::Activity, community::Community};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommunityQuery {
community_name: String,
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_apub_community_http(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(conn, &info.community_name)
})
.await??;
if !community.deleted {
let apub = community.to_apub(context.pool()).await?;
Ok(create_apub_response(&apub))
} else {
Ok(create_apub_tombstone_response(&community.to_tombstone()?))
}
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let community_id = community.id;
let community_followers = blocking(context.pool(), move |conn| {
CommunityFollowerView::for_community(&conn, community_id)
})
.await??;
let mut collection = UnorderedCollection::new();
collection
.set_many_contexts(lemmy_context()?)
.set_id(community.followers_url.into())
.set_total_items(community_followers.len() as u64);
Ok(create_apub_response(&collection))
}
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments).
pub async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let community_actor_id = community.actor_id.to_owned();
let activities = blocking(context.pool(), move |conn| {
Activity::read_community_outbox(conn, &community_actor_id)
})
.await??;
let activities = activities
.iter()
.map(AnyBase::from_arbitrary_json)
.collect::<Result<Vec<AnyBase>, serde_json::Error>>()?;
let len = activities.len();
let mut collection = OrderedCollection::new();
collection
.set_many_items(activities)
.set_many_contexts(lemmy_context()?)
.set_id(community.get_outbox_url()?)
.set_total_items(len as u64);
Ok(create_apub_response(&collection))
}
pub async fn get_apub_community_inbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", community.actor_id).parse()?)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View file

@ -1,68 +0,0 @@
use crate::APUB_JSON_CONTENT_TYPE;
use actix_web::{body::Body, web, HttpResponse};
use http::StatusCode;
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::activity::Activity_;
use lemmy_db_schema::source::activity::Activity;
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use url::Url;
pub mod comment;
pub mod community;
pub mod person;
pub mod post;
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.status(StatusCode::GONE)
.json(data)
}
#[derive(Deserialize)]
pub struct CommunityQuery {
type_: String,
id: String,
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_activity(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let settings = Settings::get();
let activity_id = Url::parse(&format!(
"{}/activities/{}/{}",
settings.get_protocol_and_hostname(),
info.type_,
info.id
))?
.into();
let activity = blocking(context.pool(), move |conn| {
Activity::read_from_apub_id(&conn, &activity_id)
})
.await??;
let sensitive = activity.sensitive.unwrap_or(true);
if !activity.local || sensitive {
Ok(HttpResponse::NotFound().finish())
} else {
Ok(create_apub_response(&activity.data))
}
}

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,35 +0,0 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
};
use actix_web::{body::Body, web, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::{source::post::Post, PostId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PostQuery {
post_id: String,
}
/// Return the ActivityPub json representation of a local post over HTTP.
pub async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let id = PostId(info.post_id.parse::<i32>()?);
let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
if !post.local {
return Err(NotFound.into());
}
if !post.deleted {
Ok(create_apub_response(&post.to_apub(context.pool()).await?))
} else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
}
}

View file

@ -1,281 +0,0 @@
use crate::{
activities::receive::verify_activity_domains_valid,
inbox::{
assert_activity_not_local,
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_public,
receive_for_community::{
receive_create_for_community,
receive_delete_for_community,
receive_dislike_for_community,
receive_like_for_community,
receive_undo_for_community,
receive_update_for_community,
},
},
insert_activity,
ActorType,
};
use activitystreams::{
activity::{kind::FollowType, ActorAndObject, Follow, Undo},
base::AnyBase,
prelude::*,
};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::community::Community_, ApubObject, DbPool, Followable};
use lemmy_db_schema::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
person::Person,
},
CommunityId,
};
use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::info;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
/// Allowed activities for community inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum CommunityValidTypes {
Follow, // follow request from a person
Undo, // unfollow from a person
Create, // create post or comment
Update, // update post or comment
Like, // upvote post or comment
Dislike, // downvote post or comment
Delete, // post or comment deleted by creator
Remove, // post or comment removed by mod or admin
}
pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
/// Handler for all incoming receive to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<CommunityAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
// Check if the activity is actually meant for us
let path = path.into_inner();
let community = blocking(&context.pool(), move |conn| {
Community::read_from_name(&conn, &path)
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
if !to_and_cc.contains(&&community.actor_id()) {
return Err(anyhow!("Activity delivered to wrong community").into());
}
assert_activity_not_local(&activity)?;
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
info!(
"Community {} received activity {:?} from {}",
community.name,
&activity.id_unchecked(),
&actor.actor_id()
);
community_receive_message(
activity.clone(),
community.clone(),
actor.as_ref(),
&context,
request_counter,
)
.await
}
/// Receives Follow, Undo/Follow, post actions, comment actions (including votes)
pub(crate) async fn community_receive_message(
activity: CommunityAcceptedActivities,
to_community: Community,
actor: &dyn ActorType,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
// Only persons can send activities to the community, so we can get the actor as person
// unconditionally.
let actor_id = actor.actor_id();
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &actor_id.into())
})
.await??;
check_community_or_site_ban(&person, to_community.id, context.pool()).await?;
let any_base = activity.clone().into_any_base()?;
let actor_url = actor.actor_id();
let activity_kind = activity.kind().context(location_info!())?;
let do_announce = match activity_kind {
CommunityValidTypes::Follow => {
handle_follow(any_base.clone(), person, &to_community, &context).await?;
false
}
CommunityValidTypes::Undo => {
handle_undo(
context,
activity.clone(),
actor_url,
&to_community,
request_counter,
)
.await?
}
CommunityValidTypes::Create => {
receive_create_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Update => {
receive_update_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Like => {
receive_like_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Dislike => {
receive_dislike_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Delete => {
receive_delete_for_community(context, any_base.clone(), &actor_url).await?;
true
}
CommunityValidTypes::Remove => {
// TODO: we dont support remote mods, so this is ignored for now
//receive_remove_for_community(context, any_base.clone(), &person_url).await?
false
}
};
if do_announce {
// Check again that the activity is public, just to be sure
is_addressed_to_public(&activity)?;
to_community
.send_announce(activity.into_any_base()?, context)
.await?;
}
Ok(HttpResponse::Ok().finish())
}
/// Handle a follow request from a remote person, adding the person as follower and returning an
/// Accept activity.
async fn handle_follow(
activity: AnyBase,
person: Person,
community: &Community,
context: &LemmyContext,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: person.id,
pending: false,
};
// This will fail if they're already a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(follow, context).await?;
Ok(HttpResponse::Ok().finish())
}
async fn handle_undo(
context: &LemmyContext,
activity: CommunityAcceptedActivities,
actor_url: Url,
to_community: &Community,
request_counter: &mut i32,
) -> Result<bool, LemmyError> {
let inner_kind = activity
.object()
.is_single_kind(&FollowType::Follow.to_string());
let any_base = activity.into_any_base()?;
if inner_kind {
handle_undo_follow(any_base, actor_url, to_community, &context).await?;
Ok(false)
} else {
receive_undo_for_community(context, any_base, &actor_url, request_counter).await?;
Ok(true)
}
}
/// Handle `Undo/Follow` from a person, removing the person from followers list.
async fn handle_undo_follow(
activity: AnyBase,
person_url: Url,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &person_url, true)?;
let object = undo.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person_url, false)?;
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &person_url.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: person.id,
pending: false,
};
// This will fail if they aren't a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
Ok(())
}
pub(crate) async fn check_community_or_site_ban(
person: &Person,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
if person.banned {
return Err(anyhow!("Person is banned from site").into());
}
let person_id = person.id;
let is_banned =
move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(anyhow!("Person is banned from community").into());
}
Ok(())
}

View file

@ -1,180 +0,0 @@
use crate::{
check_is_apub_id_valid,
extensions::signatures::verify_signature,
fetcher::get_or_fetch_and_upsert_actor,
ActorType,
};
use activitystreams::{
activity::ActorAndObjectRefExt,
base::{AsBase, BaseExt, Extends},
object::{AsObject, ObjectExt},
public,
};
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{
source::{activity::Activity_, community::Community_},
ApubObject,
DbPool,
};
use lemmy_db_schema::source::{activity::Activity, community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::Serialize;
use std::fmt::Debug;
use url::Url;
pub mod community_inbox;
pub mod person_inbox;
mod receive_for_community;
pub mod shared_inbox;
pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
where
T: BaseExt<Kind> + Extends<Kind> + Debug,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let creator_domain = creator_uri.host_str().context(location_info!())?;
let activity_id = activity.id(creator_domain)?;
Ok(activity_id.context(location_info!())?.to_owned())
}
pub(crate) async fn is_activity_already_known(
pool: &DbPool,
activity_id: &Url,
) -> Result<bool, LemmyError> {
let activity_id = activity_id.to_owned().into();
let existing = blocking(pool, move |conn| {
Activity::read_from_apub_id(&conn, &activity_id)
})
.await?;
match existing {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub(crate) fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
let mut to_and_cc = vec![];
if let Some(to) = activity.to() {
let to = to.to_owned().unwrap_to_vec();
let mut to = to
.iter()
.map(|t| t.as_xsd_any_uri())
.flatten()
.map(|t| t.to_owned())
.collect();
to_and_cc.append(&mut to);
}
if let Some(cc) = activity.cc() {
let cc = cc.to_owned().unwrap_to_vec();
let mut cc = cc
.iter()
.map(|c| c.as_xsd_any_uri())
.flatten()
.map(|c| c.to_owned())
.collect();
to_and_cc.append(&mut cc);
}
to_and_cc
}
pub(crate) fn is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.contains(&public()) {
Ok(())
} else {
Err(anyhow!("Activity is not addressed to public").into())
}
}
pub(crate) async fn inbox_verify_http_signature<T, Kind>(
activity: &T,
context: &LemmyContext,
request: HttpRequest,
request_counter: &mut i32,
) -> Result<Box<dyn ActorType>, LemmyError>
where
T: AsObject<Kind> + ActorAndObjectRefExt + Extends<Kind> + AsBase<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let actor_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
check_is_apub_id_valid(&actor_id)?;
let actor = get_or_fetch_and_upsert_actor(&actor_id, &context, request_counter).await?;
verify_signature(&request, actor.as_ref())?;
Ok(actor)
}
/// Returns true if `to_and_cc` contains at least one local user.
pub(crate) async fn is_addressed_to_local_person(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<bool, LemmyError> {
for url in to_and_cc {
let url = url.to_owned();
let person = blocking(&pool, move |conn| {
Person::read_from_apub_id(&conn, &url.into())
})
.await?;
if let Ok(u) = person {
if u.local {
return Ok(true);
}
}
}
Ok(false)
}
/// If `to_and_cc` contains the followers collection of a remote community, returns this community
/// (like `https://example.com/c/main/followers`)
pub(crate) async fn is_addressed_to_community_followers(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_owned().into();
let community = blocking(&pool, move |conn| {
// ignore errors here, because the current url might not actually be a followers url
Community::read_from_followers_url(&conn, &url).ok()
})
.await?;
if let Some(c) = community {
if !c.local {
return Ok(Some(c));
}
}
}
Ok(None)
}
pub(in crate::inbox) fn assert_activity_not_local<T, Kind>(activity: &T) -> Result<(), LemmyError>
where
T: BaseExt<Kind> + Debug,
{
let id = activity.id_unchecked().context(location_info!())?;
let activity_domain = id.domain().context(location_info!())?;
if activity_domain == Settings::get().hostname() {
return Err(
anyhow!(
"Error: received activity which was sent by local instance: {:?}",
activity
)
.into(),
);
}
Ok(())
}

View file

@ -1,420 +0,0 @@
use crate::{
activities::receive::{
comment::{receive_create_comment, receive_update_comment},
community::{
receive_delete_community,
receive_remove_community,
receive_undo_delete_community,
receive_undo_remove_community,
},
private_message::{
receive_create_private_message,
receive_delete_private_message,
receive_undo_delete_private_message,
receive_update_private_message,
},
receive_unhandled_activity,
verify_activity_domains_valid,
},
check_is_apub_id_valid,
fetcher::community::get_or_fetch_and_upsert_community,
inbox::{
assert_activity_not_local,
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_person,
is_addressed_to_public,
receive_for_community::{
receive_create_for_community,
receive_delete_for_community,
receive_dislike_for_community,
receive_like_for_community,
receive_remove_for_community,
receive_undo_for_community,
receive_update_for_community,
},
},
insert_activity,
ActorType,
};
use activitystreams::{
activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Undo, Update},
base::AnyBase,
prelude::*,
};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::person::Person_, ApubObject, Followable};
use lemmy_db_schema::source::{
community::{Community, CommunityFollower},
person::Person,
private_message::PrivateMessage,
};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use strum_macros::EnumString;
use url::Url;
/// Allowed activities for person inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum PersonValidTypes {
Accept, // community accepted our follow request
Create, // create private message
Update, // edit private message
Delete, // private message or community deleted by creator
Undo, // private message or community restored
Remove, // community removed by admin
Announce, // post, comment or vote in community
}
pub type PersonAcceptedActivities = ActorAndObject<PersonValidTypes>;
/// Handler for all incoming activities to person inboxes.
pub async fn person_inbox(
request: HttpRequest,
input: web::Json<PersonAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
// Check if the activity is actually meant for us
let username = path.into_inner();
let person = blocking(&context.pool(), move |conn| {
Person::find_by_name(&conn, &username)
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
// TODO: we should also accept activities that are sent to community followers
if !to_and_cc.contains(&&person.actor_id()) {
return Err(anyhow!("Activity delivered to wrong person").into());
}
assert_activity_not_local(&activity)?;
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
debug!(
"Person {} received activity {:?} from {}",
person.name,
&activity.id_unchecked(),
&actor.actor_id()
);
person_receive_message(
activity.clone(),
Some(person.clone()),
actor.as_ref(),
&context,
request_counter,
)
.await
}
/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete
pub(crate) async fn person_receive_message(
activity: PersonAcceptedActivities,
to_person: Option<Person>,
actor: &dyn ActorType,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
is_for_person_inbox(context, &activity).await?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().context(location_info!())?;
let actor_url = actor.actor_id();
match kind {
PersonValidTypes::Accept => {
receive_accept(
&context,
any_base,
actor,
to_person.expect("person provided"),
request_counter,
)
.await?;
}
PersonValidTypes::Announce => {
receive_announce(&context, any_base, actor, request_counter).await?
}
PersonValidTypes::Create => {
receive_create(&context, any_base, actor_url, request_counter).await?
}
PersonValidTypes::Update => {
receive_update(&context, any_base, actor_url, request_counter).await?
}
PersonValidTypes::Delete => {
receive_delete(context, any_base, &actor_url, request_counter).await?
}
PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
PersonValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?,
};
// TODO: would be logical to move websocket notification code here
Ok(HttpResponse::Ok().finish())
}
/// Returns true if the activity is addressed directly to one or more local persons, or if it is
/// addressed to the followers collection of a remote community, and at least one local person follows
/// it.
async fn is_for_person_inbox(
context: &LemmyContext,
activity: &PersonAcceptedActivities,
) -> Result<(), LemmyError> {
let to_and_cc = get_activity_to_and_cc(activity);
// Check if it is addressed directly to any local person
if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
return Ok(());
}
// Check if it is addressed to any followers collection of a remote community, and that the
// community has local followers.
let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?;
if let Some(c) = community {
let community_id = c.id;
let has_local_followers = blocking(&context.pool(), move |conn| {
CommunityFollower::has_local_followers(conn, community_id)
})
.await??;
if c.local {
return Err(
anyhow!("Remote activity cant be addressed to followers of local community").into(),
);
}
if has_local_followers {
return Ok(());
}
}
Err(anyhow!("Not addressed for any local person").into())
}
/// Handle accepted follows.
async fn receive_accept(
context: &LemmyContext,
activity: AnyBase,
actor: &dyn ActorType,
person: Person,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let accept = Accept::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&accept, &actor.actor_id(), false)?;
let object = accept.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_uri = accept
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community =
get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
let community_id = community.id;
let person_id = person.id;
// This will throw an error if no follow was requested
blocking(&context.pool(), move |conn| {
CommunityFollower::follow_accepted(conn, community_id, person_id)
})
.await??;
Ok(())
}
#[derive(EnumString)]
enum AnnouncableActivities {
Create,
Update,
Like,
Dislike,
Delete,
Remove,
Undo,
}
/// Takes an announce and passes the inner activity to the appropriate handler.
pub async fn receive_announce(
context: &LemmyContext,
activity: AnyBase,
actor: &dyn ActorType,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let announce = Announce::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
is_addressed_to_public(&announce)?;
let kind = announce
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
let inner_activity = announce
.object()
.to_owned()
.one()
.context(location_info!())?;
let inner_id = inner_activity.id().context(location_info!())?.to_owned();
check_is_apub_id_valid(&inner_id)?;
if is_activity_already_known(context.pool(), &inner_id).await? {
return Ok(());
}
use AnnouncableActivities::*;
match kind {
Some(Create) => {
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Update) => {
receive_update_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Like) => {
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Dislike) => {
receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some(Remove) => receive_remove_for_community(context, inner_activity, &inner_id).await,
Some(Undo) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
}
_ => receive_unhandled_activity(inner_activity),
}
}
async fn receive_create(
context: &LemmyContext,
activity: AnyBase,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?;
if is_addressed_to_public(&create).is_ok() {
receive_create_comment(create, context, request_counter).await
} else {
receive_create_private_message(&context, create, expected_domain, request_counter).await
}
}
async fn receive_update(
context: &LemmyContext,
activity: AnyBase,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?;
if is_addressed_to_public(&update).is_ok() {
receive_update_comment(update, context, request_counter).await
} else {
receive_update_private_message(&context, update, expected_domain, request_counter).await
}
}
async fn receive_delete(
context: &LemmyContext,
any_base: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use CommunityOrPrivateMessage::*;
let delete = Delete::from_any_base(any_base.clone())?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
let object_uri = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_community_or_private_message_by_id(context, object_uri).await? {
Community(c) => receive_delete_community(context, c).await,
PrivateMessage(p) => receive_delete_private_message(context, delete, p, request_counter).await,
}
}
async fn receive_undo(
context: &LemmyContext,
any_base: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use CommunityOrPrivateMessage::*;
let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
verify_activity_domains_valid(&undo, expected_domain, true)?;
let inner_activity = undo.object().to_owned().one().context(location_info!())?;
let kind = inner_activity.kind_str();
match kind {
Some("Delete") => {
let delete = Delete::from_any_base(inner_activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
let object_uri = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_community_or_private_message_by_id(context, object_uri).await? {
Community(c) => receive_undo_delete_community(context, undo, c, expected_domain).await,
PrivateMessage(p) => {
receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter)
.await
}
}
}
Some("Remove") => receive_undo_remove_community(context, undo, expected_domain).await,
_ => receive_unhandled_activity(undo),
}
}
enum CommunityOrPrivateMessage {
Community(Community),
PrivateMessage(PrivateMessage),
}
async fn find_community_or_private_message_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<CommunityOrPrivateMessage, LemmyError> {
let ap_id = apub_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = community {
return Ok(CommunityOrPrivateMessage::Community(c));
}
let ap_id = apub_id.to_owned();
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = private_message {
return Ok(CommunityOrPrivateMessage::PrivateMessage(p));
}
Err(NotFound.into())
}

View file

@ -1,376 +0,0 @@
use crate::{
activities::receive::{
comment::{
receive_create_comment,
receive_delete_comment,
receive_dislike_comment,
receive_like_comment,
receive_remove_comment,
receive_update_comment,
},
comment_undo::{
receive_undo_delete_comment,
receive_undo_dislike_comment,
receive_undo_like_comment,
receive_undo_remove_comment,
},
post::{
receive_create_post,
receive_delete_post,
receive_dislike_post,
receive_like_post,
receive_remove_post,
receive_update_post,
},
post_undo::{
receive_undo_delete_post,
receive_undo_dislike_post,
receive_undo_like_post,
receive_undo_remove_post,
},
receive_unhandled_activity,
verify_activity_domains_valid,
},
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
find_post_or_comment_by_id,
inbox::is_addressed_to_public,
PostOrComment,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
base::AnyBase,
prelude::*,
};
use anyhow::Context;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::site::Site;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use strum_macros::EnumString;
use url::Url;
#[derive(EnumString)]
enum PageOrNote {
Page,
Note,
}
/// This file is for post/comment activities received by the community, and for post/comment
/// activities announced by the community and received by the person.
/// A post or comment being created
pub(in crate::inbox) async fn receive_create_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?;
let kind = create
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
_ => receive_unhandled_activity(create),
}
}
/// A post or comment being edited
pub(in crate::inbox) async fn receive_update_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?;
is_addressed_to_public(&update)?;
let kind = update
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await,
Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
_ => receive_unhandled_activity(update),
}
}
/// A post or comment being upvoted
pub(in crate::inbox) async fn receive_like_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let like = Like::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?;
let object_id = like
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
PostOrComment::Comment(comment) => {
receive_like_comment(like, *comment, context, request_counter).await
}
}
}
/// A post or comment being downvoted
pub(in crate::inbox) async fn receive_dislike_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let enable_downvotes = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.enable_downvotes)
})
.await??;
if !enable_downvotes {
return Ok(());
}
let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?;
let object_id = dislike
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_dislike_post(dislike, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_dislike_comment(dislike, *comment, context, request_counter).await
}
}
}
/// A post or comment being deleted by its creator
pub(in crate::inbox) async fn receive_delete_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?;
let object = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment being removed by a mod/admin
pub(in crate::inbox) async fn receive_remove_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?;
let cc = remove
.cc()
.map(|c| c.as_many())
.flatten()
.context(location_info!())?;
let community_id = cc
.first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
let object = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
// Ensure that remove activity comes from the same domain as the community
remove.id(community_id.domain().context(location_info!())?)?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
#[derive(EnumString)]
enum UndoableActivities {
Delete,
Remove,
Like,
Dislike,
}
/// A post/comment action being reverted (either a delete, remove, upvote or downvote)
pub(in crate::inbox) async fn receive_undo_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?;
use UndoableActivities::*;
match undo
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok())
{
Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await,
Some(Like) => {
receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
}
Some(Dislike) => {
receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
}
_ => receive_unhandled_activity(undo),
}
}
/// A post or comment deletion being reverted
pub(in crate::inbox) async fn receive_undo_delete_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?;
let object = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment removal being reverted
pub(in crate::inbox) async fn receive_undo_remove_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?;
let object = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment upvote being reverted
pub(in crate::inbox) async fn receive_undo_like_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?;
let object_id = like
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_undo_like_post(&like, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_undo_like_comment(&like, *comment, context, request_counter).await
}
}
}
/// A post or comment downvote being reverted
pub(in crate::inbox) async fn receive_undo_dislike_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?;
let object_id = dislike
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_undo_dislike_post(&dislike, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
}
}
}
async fn fetch_post_or_comment_by_id(
apub_id: &Url,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<PostOrComment, LemmyError> {
if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
return Ok(PostOrComment::Post(Box::new(post)));
}
if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
return Ok(PostOrComment::Comment(Box::new(comment)));
}
Err(NotFound.into())
}

View file

@ -1,152 +0,0 @@
use crate::{
inbox::{
assert_activity_not_local,
community_inbox::{community_receive_message, CommunityAcceptedActivities},
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_person,
person_inbox::{person_receive_message, PersonAcceptedActivities},
},
insert_activity,
};
use activitystreams::{activity::ActorAndObject, prelude::*};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::source::community::Community;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
/// Allowed activity types for shared inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum ValidTypes {
Create,
Update,
Like,
Dislike,
Delete,
Undo,
Remove,
Announce,
}
// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
// but it still works due to the anybase conversion
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
/// Handler for all incoming requests to shared inbox.
pub async fn shared_inbox(
request: HttpRequest,
input: web::Json<AcceptedActivities>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let actor_id = actor.actor_id();
let activity_id = get_activity_id(&activity, &actor_id)?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
assert_activity_not_local(&activity)?;
// Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen
// if we receive the same activity twice in very quick succession.
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
let activity_any_base = activity.clone().into_any_base()?;
let mut res: Option<HttpResponse> = None;
let to_and_cc = get_activity_to_and_cc(&activity);
// Handle community first, so in case the sender is banned by the community, it will error out.
// If we handled the person receive first, the activity would be inserted to the database before the
// community could check for bans.
// Note that an activity can be addressed to a community and to a person (or multiple persons) at the
// same time. In this case we still only handle it once, to avoid duplicate websocket
// notifications.
let community = extract_local_community_from_destinations(&to_and_cc, context.pool()).await?;
if let Some(community) = community {
let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
res = Some(
community_receive_message(
community_activity,
community,
actor.as_ref(),
&context,
request_counter,
)
.await?,
);
} else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
// `to_person` is only used for follow activities (which we dont receive here), so no need to pass
// it in
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
request_counter,
)
.await?;
} else if is_addressed_to_community_followers(&to_and_cc, context.pool())
.await?
.is_some()
{
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
res = Some(
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
request_counter,
)
.await?,
);
}
// If none of those, throw an error
if let Some(r) = res {
Ok(r)
} else {
Ok(HttpResponse::NotImplemented().finish())
}
}
/// If `to_and_cc` contains the ID of a local community, return that community, otherwise return
/// None.
///
/// This doesnt handle the case where an activity is addressed to multiple communities (because
/// Lemmy doesnt generate such activities).
async fn extract_local_community_from_destinations(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_owned();
let community = blocking(&pool, move |conn| {
Community::read_from_apub_id(&conn, &url.into())
})
.await?;
if let Ok(c) = community {
if c.local {
return Ok(Some(c));
}
}
}
Ok(None)
}

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,187 +0,0 @@
use crate::{
extensions::context::lemmy_context,
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_or_fetch_and_upsert_person,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
};
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
public,
};
use anyhow::{anyhow, Context};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentForm},
person::Person,
post::Post,
},
CommentId,
};
use lemmy_utils::{
location_info,
utils::{convert_datetime, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Comment {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut comment = ApObject::new(Note::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id.into_inner()];
if let Some(parent_id) = self.parent_id {
let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
in_reply_to_vec.push(parent_comment.ap_id.into_inner());
}
comment
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(public())
.set_many_in_reply_tos(in_reply_to_vec)
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut comment, &self.content)?;
if let Some(u) = self.updated {
comment.set_updated(convert_datetime(u));
}
Ok(comment)
}
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 Comment {
type ApubType = NoteExt;
/// Converts a `Note` to `Comment`.
///
/// If the parent community, post and comment(s) are not known locally, these are also fetched.
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Comment, LemmyError> {
let comment: Comment =
get_object_from_apub(note, context, expected_domain, request_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
.await?;
if post.locked {
// This is not very efficient because a comment gets inserted just to be deleted right
// afterwards, but it seems to be the easiest way to implement it.
blocking(context.pool(), move |conn| {
Comment::delete(conn, comment.id)
})
.await??;
Err(anyhow!("Post is locked").into())
} else {
Ok(comment)
}
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for CommentForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<CommentForm, LemmyError> {
let creator_actor_id = &note
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let mut in_reply_tos = note
.in_reply_to()
.as_ref()
.context(location_info!())?
.as_many()
.context(location_info!())?
.iter()
.map(|i| i.as_xsd_any_uri().context(""));
let post_ap_id = in_reply_tos.next().context(location_info!())??;
// This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = get_or_fetch_and_insert_post(&post_ap_id, context, request_counter).await?;
// The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<CommentId> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri?;
let parent_comment =
get_or_fetch_and_insert_comment(&parent_comment_ap_id, context, request_counter).await?;
Some(parent_comment.id)
}
None => None,
};
let content = get_source_markdown_value(note)?.context(location_info!())?;
let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
content: content_slurs_removed,
removed: None,
read: None,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
ap_id: Some(check_object_domain(note, expected_domain)?),
local: false,
})
}
}

View file

@ -1,228 +0,0 @@
use crate::{
extensions::{context::lemmy_context, group_extensions::GroupExtension},
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,
},
ActorType,
GroupExt,
};
use activitystreams::{
actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt,
object::{ApObject, Image, Tombstone},
prelude::*,
};
use activitystreams_ext::Ext2;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::{
naive_now,
source::community::{Community, CommunityForm},
};
use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
use lemmy_utils::{
location_info,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Community {
type ApubType = GroupExt;
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let id = self.id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, id)
})
.await??;
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.collect();
let mut group = ApObject::new(Group::new());
group
.set_many_contexts(lemmy_context()?)
.set_id(self.actor_id.to_owned().into())
.set_name(self.title.to_owned())
.set_published(convert_datetime(self.published))
.set_many_attributed_tos(moderators);
if let Some(u) = self.updated.to_owned() {
group.set_updated(convert_datetime(u));
}
if let Some(d) = self.description.to_owned() {
set_content_and_source(&mut group, &d)?;
}
if let Some(icon_url) = &self.icon {
let mut image = Image::new();
image.set_url::<Url>(icon_url.to_owned().into());
group.set_icon(image.into_any_base()?);
}
if let Some(banner_url) = &self.banner {
let mut image = Image::new();
image.set_url::<Url>(banner_url.to_owned().into());
group.set_image(image.into_any_base()?);
}
let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
ap_actor
.set_preferred_username(self.name.to_owned())
.set_outbox(self.get_outbox_url()?)
.set_followers(self.followers_url.clone().into())
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
..Default::default()
});
Ok(Ext2::new(
ap_actor,
GroupExtension::new(self.nsfw)?,
self.get_public_key_ext()?,
))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.actor_id.to_owned().into(),
self.updated,
GroupType::Group,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for Community {
type ApubType = GroupExt;
/// Converts a `Group` to `Community`.
async fn from_apub(
group: &GroupExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Community, LemmyError> {
get_object_from_apub(group, context, expected_domain, request_counter).await
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<GroupExt> for CommunityForm {
async fn from_apub(
group: &GroupExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
let creator_uri = creator_and_moderator_uris
.as_many()
.context(location_info!())?
.iter()
.next()
.context(location_info!())?
.as_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
let name = group
.inner
.preferred_username()
.context(location_info!())?
.to_string();
let title = group
.inner
.name()
.context(location_info!())?
.as_one()
.context(location_info!())?
.as_xsd_string()
.context(location_info!())?
.to_string();
let description = get_source_markdown_value(group)?;
check_slurs(&name)?;
check_slurs(&title)?;
check_slurs_opt(&description)?;
let icon = match group.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_owned().into()),
),
None => None,
};
let banner = match group.image() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_owned().into()),
),
None => None,
};
let shared_inbox = group
.inner
.endpoints()?
.map(|e| e.shared_inbox)
.flatten()
.map(|s| s.to_owned().into());
Ok(CommunityForm {
name,
title,
description,
creator_id: creator.id,
removed: None,
published: group.inner.published().map(|u| u.to_owned().naive_local()),
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: group.ext_one.sensitive.unwrap_or(false),
actor_id: Some(check_object_domain(group, expected_domain)?),
local: false,
private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
last_refreshed_at: Some(naive_now()),
icon,
banner,
followers_url: Some(
group
.inner
.followers()?
.context(location_info!())?
.to_owned()
.into(),
),
inbox_url: Some(group.inner.inbox()?.to_owned().into()),
shared_inbox_url: Some(shared_inbox),
})
}
}

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,192 +0,0 @@
use crate::{
extensions::context::lemmy_context,
objects::{
check_object_domain,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
ActorType,
PersonExt,
};
use activitystreams::{
actor::{ApActor, Endpoints, Person},
object::{ApObject, Image, Tombstone},
prelude::*,
};
use activitystreams_ext::Ext1;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::{
naive_now,
source::person::{Person as DbPerson, PersonForm},
};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for DbPerson {
type ApubType = PersonExt;
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
let mut person = ApObject::new(Person::new());
person
.set_many_contexts(lemmy_context()?)
.set_id(self.actor_id.to_owned().into_inner())
.set_published(convert_datetime(self.published));
if let Some(u) = self.updated {
person.set_updated(convert_datetime(u));
}
if let Some(avatar_url) = &self.avatar {
let mut image = Image::new();
image.set_url::<Url>(avatar_url.to_owned().into());
person.set_icon(image.into_any_base()?);
}
if let Some(banner_url) = &self.banner {
let mut image = Image::new();
image.set_url::<Url>(banner_url.to_owned().into());
person.set_image(image.into_any_base()?);
}
if let Some(bio) = &self.bio {
set_content_and_source(&mut person, bio)?;
}
if let Some(i) = self.preferred_username.to_owned() {
person.set_name(i);
}
let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), person);
ap_actor
.set_preferred_username(self.name.to_owned())
.set_outbox(self.get_outbox_url()?)
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
..Default::default()
});
Ok(Ext1::new(ap_actor, self.get_public_key_ext()?))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!()
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for DbPerson {
type ApubType = PersonExt;
async fn from_apub(
person: &PersonExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<DbPerson, LemmyError> {
let person_id = person.id_unchecked().context(location_info!())?.to_owned();
let domain = person_id.domain().context(location_info!())?;
if domain == Settings::get().hostname() {
let person = blocking(context.pool(), move |conn| {
DbPerson::read_from_apub_id(conn, &person_id.into())
})
.await??;
Ok(person)
} else {
let person_form =
PersonForm::from_apub(person, context, expected_domain, request_counter).await?;
let person = blocking(context.pool(), move |conn| {
DbPerson::upsert(conn, &person_form)
})
.await??;
Ok(person)
}
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<PersonExt> for PersonForm {
async fn from_apub(
person: &PersonExt,
_context: &LemmyContext,
expected_domain: Url,
_request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let avatar = match person.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
),
None => None,
};
let banner = match person.image() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
),
None => None,
};
let name: String = person
.inner
.preferred_username()
.context(location_info!())?
.to_string();
let preferred_username: Option<String> = person
.name()
.map(|n| n.one())
.flatten()
.map(|n| n.to_owned().xsd_string())
.flatten();
let bio = get_source_markdown_value(person)?;
let shared_inbox = person
.inner
.endpoints()?
.map(|e| e.shared_inbox)
.flatten()
.map(|s| s.to_owned().into());
check_slurs(&name)?;
check_slurs_opt(&preferred_username)?;
check_slurs_opt(&bio)?;
Ok(PersonForm {
name,
preferred_username: Some(preferred_username),
banned: None,
deleted: None,
avatar: avatar.map(|o| o.map(|i| i.into())),
banner: banner.map(|o| o.map(|i| i.into())),
published: person.inner.published().map(|u| u.to_owned().naive_local()),
updated: person.updated().map(|u| u.to_owned().naive_local()),
actor_id: Some(check_object_domain(person, expected_domain)?),
bio: Some(bio),
local: Some(false),
private_key: None,
public_key: Some(Some(person.ext_one.public_key.to_owned().public_key_pem)),
last_refreshed_at: Some(naive_now()),
inbox_url: Some(person.inner.inbox()?.to_owned().into()),
shared_inbox_url: Some(shared_inbox),
})
}
}

View file

@ -1,223 +0,0 @@
use crate::{
extensions::{context::lemmy_context, page_extension::PageExtension},
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
get_to_community,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
PageExt,
};
use activitystreams::{
object::{kind::PageType, ApObject, Image, Page, Tombstone},
prelude::*,
public,
};
use activitystreams_ext::Ext1;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::{
self,
source::{
community::Community,
person::Person,
post::{Post, PostForm},
},
};
use lemmy_utils::{
location_info,
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, convert_datetime, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Post {
type ApubType = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = ApObject::new(Page::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
page
// Not needed when the Post is embedded in a collection (like for community outbox)
// TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_name(self.name.to_owned())
// `summary` field for compatibility with lemmy v0.9.9 and older,
// TODO: remove this after some time
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_attributed_to(creator.actor_id.into_inner());
if let Some(body) = &self.body {
set_content_and_source(&mut page, &body)?;
}
if let Some(url) = &self.url {
page.set_url::<Url>(url.to_owned().into());
}
if let Some(thumbnail_url) = &self.thumbnail_url {
let mut image = Image::new();
image.set_url::<Url>(thumbnail_url.to_owned().into());
page.set_image(image.into_any_base()?);
}
if let Some(u) = self.updated {
page.set_updated(convert_datetime(u));
}
let ext = PageExtension {
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
};
Ok(Ext1::new(page, ext))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
PageType::Page,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for Post {
type ApubType = PageExt;
/// Converts a `PageExt` to `PostForm`.
///
/// If the post's community or creator are not known locally, these are also fetched.
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Post, LemmyError> {
let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?;
check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?;
Ok(post)
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<PageExt> for PostForm {
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one;
let creator_actor_id = page
.inner
.attributed_to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let community = get_to_community(page, context, request_counter).await?;
let thumbnail_url: Option<Url> = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
None => None,
};
let url = page
.inner
.url()
.map(|u| u.as_single_xsd_any_uri())
.flatten()
.map(|u| u.to_owned());
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
if let Some(url) = &url {
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await
} else {
(None, None, None, thumbnail_url)
};
let name = page
.inner
.name()
.map(|s| s.map(|s2| s2.to_owned()))
// The following is for compatibility with lemmy v0.9.9 and older
// TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary().map(|s| s.to_owned()))
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
.to_string();
let body = get_source_markdown_value(page)?;
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url: url.map(|u| u.into()),
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: ext.comments_enabled.map(|e| !e),
published: page
.inner
.published()
.as_ref()
.map(|u| u.to_owned().naive_local()),
updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: ext.sensitive.unwrap_or(false),
stickied: ext.stickied.or(Some(false)),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(check_object_domain(page, expected_domain)?),
local: false,
})
}
}

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);
}
}

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