Adding post and comment ap_id columns. #31
|
@ -2,7 +2,6 @@
|
||||||
ui/node_modules
|
ui/node_modules
|
||||||
server/target
|
server/target
|
||||||
docker/dev/volumes
|
docker/dev/volumes
|
||||||
docker/federation/volumes
|
|
||||||
docker/federation-test/volumes
|
docker/federation-test/volumes
|
||||||
.git
|
.git
|
||||||
ansible
|
ansible
|
||||||
|
|
|
@ -6,7 +6,6 @@ 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
|
||||||
docker/federation/volumes
|
|
||||||
docker/federation-test/volumes
|
docker/federation-test/volumes
|
||||||
docker/dev/volumes
|
docker/dev/volumes
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,6 @@ In the Lemmy community we strive to go the extra step to look out for each other
|
||||||
|
|
||||||
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
|
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could’ve communicated better — remember that it’s your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
|
||||||
|
|
||||||
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/LemmyNet/lemmy](https://yerbamate.dev/LemmyNet/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
|
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/dessalines/lemmy](https://github.com/dessalines/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/).
|
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/).
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
|
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/dessalines/lemmy.svg)
|
||||||
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy)
|
[![Build Status](https://travis-ci.org/dessalines/lemmy.svg?branch=master)](https://travis-ci.org/dessalines/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/dessalines/lemmy.svg)](https://github.com/dessalines/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.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/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/dessalines/lemmy.svg)](LICENSE)
|
||||||
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
|
![GitHub stars](https://img.shields.io/github/stars/dessalines/lemmy?style=social)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -15,18 +15,18 @@
|
||||||
|
|
||||||
<h3 align="center"><a href="https://dev.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://dev.lemmy.ml">View Site</a>
|
<a href="https://dev.lemmy.ml">View Site</a>
|
||||||
·
|
·
|
||||||
<a href="https://dev.lemmy.ml/docs/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/dessalines/lemmy/issues">Report Bug</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
|
<a href="https://github.com/dessalines/lemmy/issues">Request Feature</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/LemmyNet/lemmy/blob/master/RELEASES.md">Releases</a>
|
<a href="https://github.com/dessalines/lemmy/blob/master/RELEASES.md">Releases</a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -34,17 +34,17 @@
|
||||||
|
|
||||||
Front Page|Post
|
Front Page|Post
|
||||||
---|---
|
---|---
|
||||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
|
![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), [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).
|
[Lemmy](https://github.com/dessalines/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: Federation is still in active development and the WebSocket, as well as, HTTP API are currently unstable*
|
*Note: Federation is still in active development*
|
||||||
|
|
||||||
### Why's it called Lemmy?
|
### Why's it called Lemmy?
|
||||||
|
|
||||||
|
@ -70,10 +70,10 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
- 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.
|
||||||
|
@ -108,9 +108,8 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
|
||||||
|
|
||||||
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
|
||||||
|
|
||||||
- [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).
|
|
||||||
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
|
||||||
|
|
||||||
### Crypto
|
### Crypto
|
||||||
|
@ -125,19 +124,16 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
|
||||||
- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
|
- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
|
||||||
- [Local Development](https://dev.lemmy.ml/docs/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.dev/projects/lemmy/).
|
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://riot.im/app/#/room/#rust-reddit-fediverse: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)
|
||||||
|
- [GitHub](https://github.com/dessalines/lemmy)
|
||||||
## Code Mirrors
|
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
|
||||||
|
|
||||||
- [GitHub](https://github.com/LemmyNet/lemmy)
|
|
||||||
- [Gitea](https://yerbamate.dev/LemmyNet/lemmy)
|
|
||||||
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
|
@ -1,69 +1,6 @@
|
||||||
# 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://dev.lemmy.ml/docs/administration_backup_and_restore.html)
|
|
||||||
for backup instructions.
|
|
||||||
|
|
||||||
**With Ansible:**
|
|
||||||
|
|
||||||
```
|
|
||||||
# deploy with ansible from your local lemmy git repo
|
|
||||||
git pull
|
|
||||||
cd ansible
|
|
||||||
ansible-playbook lemmy.yml
|
|
||||||
# connect via ssh to run the migration script
|
|
||||||
ssh your-server
|
|
||||||
cd /lemmy/
|
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
|
|
||||||
chmod +x migrate-pictshare-to-pictrs.bash
|
|
||||||
sudo ./migrate-pictshare-to-pictrs.bash
|
|
||||||
```
|
|
||||||
|
|
||||||
**With manual Docker installation:**
|
|
||||||
```
|
|
||||||
# run these commands on your server
|
|
||||||
cd /lemmy
|
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
|
|
||||||
# Replace the {{ vars }}
|
|
||||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
|
||||||
sudo nginx -s reload
|
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
|
|
||||||
chmod +x migrate-pictshare-to-pictrs.bash
|
|
||||||
sudo bash migrate-pictshare-to-pictrs.bash
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** After upgrading, all users need to reload the page, then logout and
|
|
||||||
login again, so that images are loaded correctly.
|
|
||||||
|
|
||||||
# Lemmy v0.6.0 Release (2020-01-16)
|
# 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/dessalines/lemmy/milestone/15?closed=1)
|
||||||
|
|
||||||
This is the biggest release by far:
|
This is the biggest release by far:
|
||||||
|
|
||||||
|
@ -73,7 +10,7 @@ This is the biggest release by far:
|
||||||
- Can set a custom language.
|
- Can set a custom language.
|
||||||
- Lemmy-wide settings to disable downvotes, and close registration.
|
- Lemmy-wide settings to disable downvotes, and close registration.
|
||||||
- A better documentation system, hosted in lemmy itself.
|
- A better documentation system, hosted in lemmy itself.
|
||||||
- [Huge DB performance gains](https://github.com/LemmyNet/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
|
- [Huge DB performance gains](https://github.com/dessalines/lemmy/issues/411) (everthing down to < `30ms`) by using materialized views.
|
||||||
- Fixed major issue with similar post URL and title searching.
|
- Fixed major issue with similar post URL and title searching.
|
||||||
- Upgraded to Actix `2.0`
|
- Upgraded to Actix `2.0`
|
||||||
- Faster comment / post voting.
|
- Faster comment / post voting.
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
v0.7.6
|
v0.6.44
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
[defaults]
|
[defaults]
|
||||||
inventory=inventory
|
inventory=inventory
|
||||||
interpreter_python=/usr/bin/python3
|
|
||||||
|
|
||||||
[ssh_connection]
|
[ssh_connection]
|
||||||
pipelining = True
|
pipelining = True
|
||||||
|
|
|
@ -24,11 +24,10 @@
|
||||||
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||||
|
|
||||||
- name: create lemmy folder
|
- name: create lemmy folder
|
||||||
file: path={{item.path}} {{item.owner}} state=directory
|
file: path={{item.path}} state=directory
|
||||||
with_items:
|
with_items:
|
||||||
- { path: '/lemmy/', owner: 'root' }
|
- { path: '/lemmy/' }
|
||||||
- { path: '/lemmy/volumes/', owner: 'root' }
|
- { path: '/lemmy/volumes/' }
|
||||||
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: add template files
|
- name: add template files
|
||||||
|
@ -60,7 +59,6 @@
|
||||||
project_src: /lemmy/
|
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
|
||||||
|
|
|
@ -26,11 +26,10 @@
|
||||||
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
|
||||||
|
|
||||||
- name: create lemmy folder
|
- name: create lemmy folder
|
||||||
file: path={{item.path}} owner={{item.owner}} state=directory
|
file: path={{item.path}} state=directory
|
||||||
with_items:
|
with_items:
|
||||||
- { path: '/lemmy/', owner: 'root' }
|
- { path: '/lemmy/' }
|
||||||
- { path: '/lemmy/volumes/', owner: 'root' }
|
- { path: '/lemmy/volumes/' }
|
||||||
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
|
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: add template files
|
- name: add template files
|
||||||
|
@ -89,7 +88,6 @@
|
||||||
project_src: /lemmy/
|
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
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
{
|
{
|
||||||
# for more info about the config, check out the documentation
|
|
||||||
# https://dev.lemmy.ml/docs/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 "dev.lemmy.ml")
|
|
||||||
hostname: "{{ domain }}"
|
hostname: "{{ domain }}"
|
||||||
# json web token for authorization between server and client
|
|
||||||
jwt_secret: "{{ jwt_password }}"
|
jwt_secret: "{{ jwt_password }}"
|
||||||
# The location of the frontend
|
|
||||||
front_end_dir: "/app/dist"
|
front_end_dir: "/app/dist"
|
||||||
# email sending configuration
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ services:
|
||||||
- ./lemmy.hjson:/config/config.hjson:ro
|
- ./lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- pictrs
|
- pictshare
|
||||||
- iframely
|
- iframely
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -25,17 +25,16 @@ services:
|
||||||
- ./volumes/postgres:/var/lib/postgresql/data
|
- ./volumes/postgres:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
pictrs:
|
pictshare:
|
||||||
image: asonix/pictrs:amd64-v0.1.0-r9
|
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
|
||||||
|
|
||||||
iframely:
|
iframely:
|
||||||
image: jolt/iframely:v1.4.3
|
image: dogbin/iframely:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8061:80"
|
- "127.0.0.1:8061:80"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
|
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
|
||||||
limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s;
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
@ -37,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
|
||||||
|
@ -49,11 +48,8 @@ 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;
|
||||||
|
|
||||||
# Rate limit
|
|
||||||
limit_req zone=lemmy_ratelimit burst=30 nodelay;
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://0.0.0.0:8536;
|
proxy_pass http://0.0.0.0:8536;
|
||||||
|
@ -74,21 +70,15 @@ server {
|
||||||
proxy_cache_min_uses 5;
|
proxy_cache_min_uses 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Redirect pictshare images to pictrs
|
location /pictshare/ {
|
||||||
location ~ /pictshare/(.*)$ {
|
proxy_pass http://0.0.0.0:8537/;
|
||||||
return 301 /pictrs/image/$1;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
# pict-rs images
|
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
|
||||||
location /pictrs {
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
location /pictrs/image {
|
}
|
||||||
proxy_pass http://0.0.0.0:8537/image;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
# Block the import
|
|
||||||
return 403;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location /iframely/ {
|
location /iframely/ {
|
||||||
|
|
|
@ -21,13 +21,17 @@ COPY server/Cargo.toml server/Cargo.lock ./
|
||||||
RUN sudo chown -R rust:rust .
|
RUN sudo chown -R rust:rust .
|
||||||
RUN mkdir -p ./src/bin \
|
RUN mkdir -p ./src/bin \
|
||||||
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||||
RUN cargo build
|
RUN cargo build --release
|
||||||
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
|
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
|
||||||
COPY server/src ./src/
|
COPY server/src ./src/
|
||||||
COPY server/migrations ./migrations/
|
COPY server/migrations ./migrations/
|
||||||
|
|
||||||
# Build for debug
|
# Build for release
|
||||||
RUN cargo build
|
RUN cargo build --frozen --release
|
||||||
|
|
||||||
|
# Get diesel-cli on there just in case
|
||||||
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
|
||||||
FROM ekidd/rust-musl-builder:1.42.0-openssl11 as docs
|
FROM ekidd/rust-musl-builder:1.42.0-openssl11 as docs
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -35,14 +39,15 @@ COPY docs ./docs
|
||||||
RUN sudo chown -R rust:rust .
|
RUN sudo chown -R rust:rust .
|
||||||
RUN mdbook build docs/
|
RUN mdbook build docs/
|
||||||
|
|
||||||
FROM alpine:3.12
|
|
||||||
|
FROM alpine:3.10
|
||||||
|
|
||||||
# Install libpq for postgres
|
# Install libpq for postgres
|
||||||
RUN apk add libpq
|
RUN apk add libpq
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
COPY server/config/defaults.hjson /config/defaults.hjson
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
|
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy
|
||||||
COPY --from=docs /app/docs/book/ /app/dist/documentation/
|
COPY --from=docs /app/docs/book/ /app/dist/documentation/
|
||||||
COPY --from=node /app/ui/dist /app/dist
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
FROM node:10-jessie as node
|
||||||
|
|
||||||
|
WORKDIR /app/ui
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
COPY ui/package.json ui/yarn.lock ./
|
||||||
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
|
# Build
|
||||||
|
COPY ui /app/ui
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
|
||||||
|
# contains qemu-*-static for cross-compilation
|
||||||
|
FROM multiarch/qemu-user-static as qemu
|
||||||
|
|
||||||
|
|
||||||
|
FROM arm64v8/rust:1.40-buster as rust
|
||||||
|
|
||||||
|
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||||
|
#COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||||
|
|
||||||
|
|
||||||
|
# Install musl
|
||||||
|
#RUN apt-get update && apt-get install -y mc
|
||||||
|
#RUN apt-get install -y musl-tools mc
|
||||||
|
#libpq-dev mc
|
||||||
|
#RUN rustup target add ${TARGET}
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
WORKDIR /app
|
||||||
|
RUN USER=root cargo new server
|
||||||
|
WORKDIR /app/server
|
||||||
|
COPY server/Cargo.toml server/Cargo.lock ./
|
||||||
|
RUN mkdir -p ./src/bin \
|
||||||
|
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||||
|
RUN cargo build --release
|
||||||
|
# RUN cargo build
|
||||||
|
COPY server/src ./src/
|
||||||
|
COPY server/migrations ./migrations/
|
||||||
|
RUN rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
|
||||||
|
|
||||||
|
|
||||||
|
# build for release
|
||||||
|
RUN cargo build --frozen --release
|
||||||
|
# RUN cargo build --frozen
|
||||||
|
|
||||||
|
# Get diesel-cli on there just in case
|
||||||
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
# RUN cp /app/server/target/debug/lemmy_server /app/server/ready
|
||||||
|
RUN cp /app/server/target/release/lemmy_server /app/server/ready
|
||||||
|
|
||||||
|
#FROM alpine:3.10
|
||||||
|
# debian because build with dynamic linking with debian:buster
|
||||||
|
FROM arm64v8/debian:buster-slim as lemmy
|
||||||
|
|
||||||
|
#COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||||
|
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||||
|
|
||||||
|
# Install libpq for postgres
|
||||||
|
#RUN apk add libpq
|
||||||
|
RUN apt-get update && apt-get install -y libpq5
|
||||||
|
|
||||||
|
RUN addgroup --gid 1000 lemmy
|
||||||
|
# for alpine
|
||||||
|
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
|
# for debian
|
||||||
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
|
USER lemmy
|
||||||
|
EXPOSE 8536
|
||||||
|
CMD ["/app/lemmy"]
|
|
@ -0,0 +1,79 @@
|
||||||
|
FROM node:10-jessie as node
|
||||||
|
|
||||||
|
WORKDIR /app/ui
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
COPY ui/package.json ui/yarn.lock ./
|
||||||
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
|
# Build
|
||||||
|
COPY ui /app/ui
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
|
||||||
|
# contains qemu-*-static for cross-compilation
|
||||||
|
FROM multiarch/qemu-user-static as qemu
|
||||||
|
|
||||||
|
|
||||||
|
FROM arm32v7/rust:1.37-buster as rust
|
||||||
|
|
||||||
|
#COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||||
|
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||||
|
|
||||||
|
|
||||||
|
# Install musl
|
||||||
|
#RUN apt-get update && apt-get install -y mc
|
||||||
|
#RUN apt-get install -y musl-tools mc
|
||||||
|
#libpq-dev mc
|
||||||
|
#RUN rustup target add ${TARGET}
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
WORKDIR /app
|
||||||
|
RUN USER=root cargo new server
|
||||||
|
WORKDIR /app/server
|
||||||
|
COPY server/Cargo.toml server/Cargo.lock ./
|
||||||
|
RUN mkdir -p ./src/bin \
|
||||||
|
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||||
|
#RUN cargo build --release
|
||||||
|
# RUN cargo build
|
||||||
|
RUN RUSTFLAGS='-Ccodegen-units=1' cargo build
|
||||||
|
COPY server/src ./src/
|
||||||
|
COPY server/migrations ./migrations/
|
||||||
|
RUN rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
|
||||||
|
|
||||||
|
|
||||||
|
# build for release
|
||||||
|
#RUN cargo build --frozen --release
|
||||||
|
RUN cargo build --frozen
|
||||||
|
|
||||||
|
# Get diesel-cli on there just in case
|
||||||
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
RUN cp /app/server/target/debug/lemmy_server /app/server/ready
|
||||||
|
#RUN cp /app/server/target/release/lemmy_server /app/server/ready
|
||||||
|
|
||||||
|
#FROM alpine:3.10
|
||||||
|
# debian because build with dynamic linking with debian:buster
|
||||||
|
FROM arm32v7/debian:buster-slim as lemmy
|
||||||
|
|
||||||
|
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||||
|
|
||||||
|
# Install libpq for postgres
|
||||||
|
#RUN apk add libpq
|
||||||
|
RUN apt-get update && apt-get install -y libpq5
|
||||||
|
|
||||||
|
RUN addgroup --gid 1000 lemmy
|
||||||
|
# for alpine
|
||||||
|
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
|
# for debian
|
||||||
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
|
||||||
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
|
USER lemmy
|
||||||
|
EXPOSE 8536
|
||||||
|
CMD ["/app/lemmy"]
|
|
@ -0,0 +1,88 @@
|
||||||
|
# can be build on x64, arm32, arm64 platforms
|
||||||
|
# to build on target platform run
|
||||||
|
# docker build -f Dockerfile.libc -t dessalines/lemmy:version ../..
|
||||||
|
#
|
||||||
|
# to use docker buildx run
|
||||||
|
# docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile.libc -t YOURNAME/lemmy --push ../..
|
||||||
|
|
||||||
|
FROM node:12-buster as node
|
||||||
|
# use this if use docker buildx
|
||||||
|
#FROM --platform=$BUILDPLATFORM node:12-buster as node
|
||||||
|
|
||||||
|
WORKDIR /app/ui
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
COPY ui/package.json ui/yarn.lock ./
|
||||||
|
RUN yarn install --pure-lockfile --network-timeout 100000
|
||||||
|
|
||||||
|
# Build
|
||||||
|
COPY ui /app/ui
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
|
||||||
|
FROM rust:1.42 as rust
|
||||||
|
|
||||||
|
# Cache deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN USER=root cargo new server
|
||||||
|
WORKDIR /app/server
|
||||||
|
COPY server/Cargo.toml server/Cargo.lock ./
|
||||||
|
RUN mkdir -p ./src/bin \
|
||||||
|
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
||||||
|
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
#RUN cargo build && \
|
||||||
|
# rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
|
||||||
|
COPY server/src ./src/
|
||||||
|
COPY server/migrations ./migrations/
|
||||||
|
|
||||||
|
|
||||||
|
# build for release
|
||||||
|
# workaround for https://github.com/rust-lang/rust/issues/62896
|
||||||
|
#RUN RUSTFLAGS='-Ccodegen-units=1' cargo build --release
|
||||||
|
RUN cargo build --release --frozen
|
||||||
|
#RUN cargo build --frozen
|
||||||
|
|
||||||
|
# Get diesel-cli on there just in case
|
||||||
|
# RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
# make result place always the same for lemmy container
|
||||||
|
RUN cp /app/server/target/release/lemmy_server /app/server/ready
|
||||||
|
#RUN cp /app/server/target/debug/lemmy_server /app/server/ready
|
||||||
|
|
||||||
|
|
||||||
|
FROM rust:1.42 as docs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Build docs
|
||||||
|
COPY docs ./docs
|
||||||
|
RUN cargo install mdbook
|
||||||
|
RUN mdbook build docs/
|
||||||
|
|
||||||
|
|
||||||
|
#FROM alpine:3.10
|
||||||
|
# debian because build with dynamic linking with debian:buster
|
||||||
|
FROM debian:buster as lemmy
|
||||||
|
|
||||||
|
# Install libpq for postgres
|
||||||
|
#RUN apk add libpq
|
||||||
|
RUN apt-get update && apt-get install -y libpq5
|
||||||
|
RUN addgroup --gid 1000 lemmy
|
||||||
|
# for alpine
|
||||||
|
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
||||||
|
# for debian
|
||||||
|
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
|
||||||
|
|
||||||
|
# Copy resources
|
||||||
|
COPY server/config/defaults.hjson /config/defaults.hjson
|
||||||
|
COPY --from=node /app/ui/dist /app/dist
|
||||||
|
COPY --from=docs /app/docs/book/ /app/dist/documentation/
|
||||||
|
COPY --from=rust /app/server/ready /app/lemmy
|
||||||
|
|
||||||
|
RUN chown lemmy:lemmy /app/lemmy
|
||||||
|
USER lemmy
|
||||||
|
EXPOSE 8536
|
||||||
|
CMD ["/app/lemmy"]
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/bin/sh
|
||||||
|
git checkout master
|
||||||
|
|
||||||
|
# Import translations
|
||||||
|
git fetch weblate
|
||||||
|
git merge weblate/master
|
||||||
|
|
||||||
|
# Creating the new tag
|
||||||
|
new_tag="$1"
|
||||||
|
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
||||||
|
|
||||||
|
# Setting the version on the front end
|
||||||
|
cd ../../
|
||||||
|
echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
|
||||||
|
git add "ui/src/version.ts"
|
||||||
|
# Setting the version on the backend
|
||||||
|
echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
|
||||||
|
git add "server/src/version.rs"
|
||||||
|
# Setting the version for Ansible
|
||||||
|
echo $new_tag > "ansible/VERSION"
|
||||||
|
git add "ansible/VERSION"
|
||||||
|
|
||||||
|
cd docker/dev || exit
|
||||||
|
|
||||||
|
# Changing the docker-compose prod
|
||||||
|
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
|
||||||
|
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../../ansible/templates/docker-compose.yml
|
||||||
|
git add ../prod/docker-compose.yml
|
||||||
|
git add ../../ansible/templates/docker-compose.yml
|
||||||
|
|
||||||
|
# The commit
|
||||||
|
git commit -m"Version $new_tag"
|
||||||
|
git tag $new_tag
|
||||||
|
|
||||||
|
# Rebuilding docker
|
||||||
|
docker-compose build
|
||||||
|
docker tag dev_lemmy:latest dessalines/lemmy:x64-$new_tag
|
||||||
|
docker push dessalines/lemmy:x64-$new_tag
|
||||||
|
|
||||||
|
# Build for Raspberry Pi / other archs
|
||||||
|
|
||||||
|
# Arm currently not working
|
||||||
|
# docker build -t lemmy:armv7hf -f Dockerfile.armv7hf ../../
|
||||||
|
# docker tag lemmy:armv7hf dessalines/lemmy:armv7hf-$new_tag
|
||||||
|
# docker push dessalines/lemmy:armv7hf-$new_tag
|
||||||
|
|
||||||
|
# aarch64
|
||||||
|
# Only do this on major releases (IE the third semver is 0)
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
|
# Registering qemu binaries
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||||
|
|
||||||
|
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../
|
||||||
|
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag
|
||||||
|
docker push dessalines/lemmy:arm64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Creating the manifest for the multi-arch build
|
||||||
|
if [ $third_semver -eq 0 ]; then
|
||||||
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
|
dessalines/lemmy:x64-$new_tag \
|
||||||
|
dessalines/lemmy:arm64-$new_tag
|
||||||
|
else
|
||||||
|
docker manifest create dessalines/lemmy:$new_tag \
|
||||||
|
dessalines/lemmy:x64-$new_tag
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker manifest push dessalines/lemmy:$new_tag
|
||||||
|
|
||||||
|
# Push
|
||||||
|
git push origin $new_tag
|
||||||
|
git push
|
||||||
|
|
||||||
|
# Pushing to any ansible deploys
|
||||||
|
cd ../../ansible || exit
|
||||||
|
ansible-playbook lemmy.yml --become
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Building from the dev branch for dev servers
|
||||||
|
git checkout dev
|
||||||
|
|
||||||
|
# Rebuilding dev docker
|
||||||
|
docker-compose build
|
||||||
|
docker tag dev_lemmy:latest dessalines/lemmy:dev
|
||||||
|
docker push dessalines/lemmy:dev
|
||||||
|
|
||||||
|
# SSH and pull it
|
||||||
|
ssh $LEMMY_USER@$LEMMY_HOST "cd ~/git/lemmy/docker/dev && docker pull dessalines/lemmy:dev && docker-compose up -d"
|
|
@ -1,6 +1,15 @@
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
|
||||||
lemmy:
|
lemmy:
|
||||||
build:
|
build:
|
||||||
|
@ -12,35 +21,22 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=debug
|
- RUST_LOG=debug
|
||||||
volumes:
|
volumes:
|
||||||
- ../lemmy.hjson:/config/config.hjson
|
- ../lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- pictrs
|
|
||||||
- postgres
|
- postgres
|
||||||
|
- pictshare
|
||||||
- iframely
|
- iframely
|
||||||
|
|
||||||
postgres:
|
pictshare:
|
||||||
image: postgres:12-alpine
|
image: shtripok/pictshare:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:5432:5432"
|
- "127.0.0.1:8537:80"
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=lemmy
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
- POSTGRES_DB=lemmy
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/postgres:/var/lib/postgresql/data
|
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
|
||||||
|
|
||||||
pictrs:
|
|
||||||
image: asonix/pictrs:v0.1.13-r0
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:8537:8080"
|
|
||||||
user: 991:991
|
|
||||||
volumes:
|
|
||||||
- ./volumes/pictrs:/mnt
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
iframely:
|
iframely:
|
||||||
image: jolt/iframely:v1.4.3
|
image: dogbin/iframely:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8061:80"
|
- "127.0.0.1:8061:80"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -1,6 +1,2 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
|
||||||
|
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
docker-compose up -d --no-deps --build
|
docker-compose up -d --no-deps --build
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BRANCH=$1
|
|
||||||
|
|
||||||
git checkout $BRANCH
|
|
||||||
|
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
|
|
||||||
# Rebuilding dev docker
|
|
||||||
sudo docker build ../../ -f . -t "dessalines/lemmy:$BRANCH"
|
|
||||||
sudo docker push "dessalines/lemmy:$BRANCH"
|
|
||||||
|
|
||||||
# Run the playbook
|
|
||||||
pushd ../../../lemmy-ansible
|
|
||||||
ansible-playbook -i test playbooks/site.yml
|
|
||||||
popd
|
|
|
@ -1,9 +1,9 @@
|
||||||
FROM ekidd/rust-musl-builder:1.42.0-openssl11
|
FROM ekidd/rust-musl-builder:1.38.0-openssl11
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
RUN mkdir /app/dist/documentation/ -p \
|
RUN mkdir /app/dist/documentation/ -p \
|
||||||
&& addgroup --gid 1001 lemmy \
|
&& addgroup --gid 1001 lemmy \
|
||||||
&& adduser --gecos "" --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
|
&& adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
|
||||||
|
|
||||||
# Copy resources
|
# Copy resources
|
||||||
COPY server/config/defaults.hjson /app/config/defaults.hjson
|
COPY server/config/defaults.hjson /app/config/defaults.hjson
|
|
@ -0,0 +1,83 @@
|
||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.17-alpine
|
||||||
|
ports:
|
||||||
|
- "8540:8540"
|
||||||
|
- "8550:8550"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
depends_on:
|
||||||
|
- lemmy_alpha
|
||||||
|
- pictshare_alpha
|
||||||
|
- lemmy_beta
|
||||||
|
- pictshare_beta
|
||||||
|
- iframely
|
||||||
|
restart: "always"
|
||||||
|
|
||||||
|
lemmy_alpha:
|
||||||
|
image: lemmy-federation-test:latest
|
||||||
|
environment:
|
||||||
|
- LEMMY_HOSTNAME=lemmy_alpha:8540
|
||||||
|
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
|
||||||
|
- LEMMY_JWT_SECRET=changeme
|
||||||
|
- LEMMY_FRONT_END_DIR=/app/dist
|
||||||
|
- LEMMY_FEDERATION__ENABLED=true
|
||||||
|
- LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_beta:8550
|
||||||
|
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||||
|
- LEMMY_PORT=8540
|
||||||
|
- RUST_BACKTRACE=1
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgres_alpha
|
||||||
|
postgres_alpha:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres_alpha:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
pictshare_alpha:
|
||||||
|
image: shtripok/pictshare:latest
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pictshare_alpha:/usr/share/nginx/html/data
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
lemmy_beta:
|
||||||
|
image: lemmy-federation-test:latest
|
||||||
|
environment:
|
||||||
|
- LEMMY_HOSTNAME=lemmy_beta:8550
|
||||||
|
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
|
||||||
|
- LEMMY_JWT_SECRET=changeme
|
||||||
|
- LEMMY_FRONT_END_DIR=/app/dist
|
||||||
|
- LEMMY_FEDERATION__ENABLED=true
|
||||||
|
- LEMMY_FEDERATION__FOLLOWED_INSTANCES=lemmy_alpha:8540
|
||||||
|
- LEMMY_FEDERATION__TLS_ENABLED=false
|
||||||
|
- LEMMY_PORT=8550
|
||||||
|
- RUST_BACKTRACE=1
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgres_beta
|
||||||
|
postgres_beta:
|
||||||
|
image: postgres:12-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=lemmy
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=lemmy
|
||||||
|
volumes:
|
||||||
|
- ./volumes/postgres_beta:/var/lib/postgresql/data
|
||||||
|
restart: always
|
||||||
|
pictshare_beta:
|
||||||
|
image: shtripok/pictshare:latest
|
||||||
|
volumes:
|
||||||
|
- ./volumes/pictshare_beta:/usr/share/nginx/html/data
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
iframely:
|
||||||
|
image: dogbin/iframely:latest
|
||||||
|
volumes:
|
||||||
|
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||||
|
restart: always
|
|
@ -0,0 +1,73 @@
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 8540;
|
||||||
|
server_name 127.0.0.1;
|
||||||
|
|
||||||
|
# Upload limit for pictshare
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://lemmy_alpha:8540;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /pictshare/ {
|
||||||
|
proxy_pass http://pictshare_alpha:80/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /iframely/ {
|
||||||
|
proxy_pass http://iframely:80/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 8550;
|
||||||
|
server_name 127.0.0.1;
|
||||||
|
|
||||||
|
# Upload limit for pictshare
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://lemmy_beta:8550;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /pictshare/ {
|
||||||
|
proxy_pass http://pictshare_beta:80/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /iframely/ {
|
||||||
|
proxy_pass http://iframely:80/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
pushd ../../ui/ || exit
|
||||||
|
yarn build
|
||||||
|
popd || exit
|
||||||
|
|
||||||
|
pushd ../../server/ || exit
|
||||||
|
cargo build
|
||||||
|
popd || exit
|
||||||
|
|
||||||
|
sudo docker build ../../ -f Dockerfile -t lemmy-federation-test:latest
|
||||||
|
|
||||||
|
sudo docker-compose up
|
|
@ -1,32 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
pushd ../../server/
|
|
||||||
cargo build
|
|
||||||
popd
|
|
||||||
|
|
||||||
pushd ../../ui
|
|
||||||
yarn
|
|
||||||
popd
|
|
||||||
|
|
||||||
mkdir -p volumes/pictrs_{alpha,beta,gamma}
|
|
||||||
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
|
|
||||||
|
|
||||||
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
|
|
||||||
|
|
||||||
sudo mkdir -p volumes/pictrs_alpha
|
|
||||||
sudo chown -R 991:991 volumes/pictrs_alpha
|
|
||||||
|
|
||||||
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
|
|
||||||
|
|
||||||
pushd ../../ui
|
|
||||||
echo "Waiting for Lemmy to start..."
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
yarn api-test || true
|
|
||||||
popd
|
|
||||||
|
|
||||||
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
|
|
||||||
|
|
||||||
sudo rm -r volumes/
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
sudo rm -rf volumes
|
|
||||||
|
|
||||||
pushd ../../server/
|
|
||||||
cargo build
|
|
||||||
popd
|
|
||||||
|
|
||||||
pushd ../../ui
|
|
||||||
yarn
|
|
||||||
popd
|
|
||||||
|
|
||||||
mkdir -p volumes/pictrs_{alpha,beta,gamma}
|
|
||||||
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
|
|
||||||
|
|
||||||
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
|
|
||||||
|
|
||||||
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up
|
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -xe
|
|
||||||
|
|
||||||
pushd ../../ui
|
|
||||||
echo "Waiting for Lemmy to start..."
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
|
|
||||||
yarn api-test || true
|
|
||||||
popd
|
|
|
@ -1,112 +0,0 @@
|
||||||
version: '3.3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
nginx:
|
|
||||||
image: nginx:1.17-alpine
|
|
||||||
ports:
|
|
||||||
- "8540:8540"
|
|
||||||
- "8550:8550"
|
|
||||||
- "8560:8560"
|
|
||||||
volumes:
|
|
||||||
# Hack to make this work from both docker/federation/ and docker/federation-test/
|
|
||||||
- ../federation/nginx.conf:/etc/nginx/nginx.conf
|
|
||||||
restart: on-failure
|
|
||||||
depends_on:
|
|
||||||
- lemmy-alpha
|
|
||||||
- pictrs
|
|
||||||
- lemmy-beta
|
|
||||||
- lemmy-gamma
|
|
||||||
- iframely
|
|
||||||
|
|
||||||
pictrs:
|
|
||||||
restart: always
|
|
||||||
image: asonix/pictrs:v0.1.13-r0
|
|
||||||
user: 991:991
|
|
||||||
volumes:
|
|
||||||
- ./volumes/pictrs_alpha:/mnt
|
|
||||||
|
|
||||||
lemmy-alpha:
|
|
||||||
image: lemmy-federation:latest
|
|
||||||
environment:
|
|
||||||
- LEMMY_HOSTNAME=lemmy-alpha:8540
|
|
||||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
|
|
||||||
- LEMMY_JWT_SECRET=changeme
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- LEMMY_FEDERATION__ENABLED=true
|
|
||||||
- LEMMY_FEDERATION__TLS_ENABLED=false
|
|
||||||
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma
|
|
||||||
- LEMMY_PORT=8540
|
|
||||||
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
|
|
||||||
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
|
||||||
- LEMMY_SETUP__SITE_NAME=lemmy-alpha
|
|
||||||
- RUST_BACKTRACE=1
|
|
||||||
- RUST_LOG=debug
|
|
||||||
depends_on:
|
|
||||||
- postgres_alpha
|
|
||||||
postgres_alpha:
|
|
||||||
image: postgres:12-alpine
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=lemmy
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
- POSTGRES_DB=lemmy
|
|
||||||
volumes:
|
|
||||||
- ./volumes/postgres_alpha:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
lemmy-beta:
|
|
||||||
image: lemmy-federation:latest
|
|
||||||
environment:
|
|
||||||
- LEMMY_HOSTNAME=lemmy-beta:8550
|
|
||||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
|
|
||||||
- LEMMY_JWT_SECRET=changeme
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- LEMMY_FEDERATION__ENABLED=true
|
|
||||||
- LEMMY_FEDERATION__TLS_ENABLED=false
|
|
||||||
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma
|
|
||||||
- LEMMY_PORT=8550
|
|
||||||
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
|
|
||||||
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
|
||||||
- LEMMY_SETUP__SITE_NAME=lemmy-beta
|
|
||||||
- RUST_BACKTRACE=1
|
|
||||||
- RUST_LOG=debug
|
|
||||||
depends_on:
|
|
||||||
- postgres_beta
|
|
||||||
postgres_beta:
|
|
||||||
image: postgres:12-alpine
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=lemmy
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
- POSTGRES_DB=lemmy
|
|
||||||
volumes:
|
|
||||||
- ./volumes/postgres_beta:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
lemmy-gamma:
|
|
||||||
image: lemmy-federation:latest
|
|
||||||
environment:
|
|
||||||
- LEMMY_HOSTNAME=lemmy-gamma:8560
|
|
||||||
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
|
|
||||||
- LEMMY_JWT_SECRET=changeme
|
|
||||||
- LEMMY_FRONT_END_DIR=/app/dist
|
|
||||||
- LEMMY_FEDERATION__ENABLED=true
|
|
||||||
- LEMMY_FEDERATION__TLS_ENABLED=false
|
|
||||||
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta
|
|
||||||
- LEMMY_PORT=8560
|
|
||||||
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
|
|
||||||
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
|
|
||||||
- LEMMY_SETUP__SITE_NAME=lemmy-gamma
|
|
||||||
- RUST_BACKTRACE=1
|
|
||||||
- RUST_LOG=debug
|
|
||||||
depends_on:
|
|
||||||
- postgres_gamma
|
|
||||||
postgres_gamma:
|
|
||||||
image: postgres:12-alpine
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=lemmy
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
- POSTGRES_DB=lemmy
|
|
||||||
volumes:
|
|
||||||
- ./volumes/postgres_gamma:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
iframely:
|
|
||||||
image: jolt/iframely:v1.4.3
|
|
||||||
volumes:
|
|
||||||
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
|
|
@ -1,125 +0,0 @@
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
server {
|
|
||||||
listen 8540;
|
|
||||||
server_name 127.0.0.1;
|
|
||||||
access_log off;
|
|
||||||
|
|
||||||
# Upload limit for pictshare
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://lemmy-alpha:8540;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# pict-rs images
|
|
||||||
location /pictrs {
|
|
||||||
location /pictrs/image {
|
|
||||||
proxy_pass http://pictrs:8080/image;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
# Block the import
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /iframely/ {
|
|
||||||
proxy_pass http://iframely:80/;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 8550;
|
|
||||||
server_name 127.0.0.1;
|
|
||||||
access_log off;
|
|
||||||
|
|
||||||
# Upload limit for pictshare
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://lemmy-beta:8550;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# pict-rs images
|
|
||||||
location /pictrs {
|
|
||||||
location /pictrs/image {
|
|
||||||
proxy_pass http://pictrs:8080/image;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
# Block the import
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /iframely/ {
|
|
||||||
proxy_pass http://iframely:80/;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 8560;
|
|
||||||
server_name 127.0.0.1;
|
|
||||||
access_log off;
|
|
||||||
|
|
||||||
# Upload limit for pictshare
|
|
||||||
client_max_body_size 50M;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://lemmy-gamma:8560;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
|
||||||
# WebSocket support
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
# pict-rs images
|
|
||||||
location /pictrs {
|
|
||||||
location /pictrs/image {
|
|
||||||
proxy_pass http://pictrs:8080/image;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
# Block the import
|
|
||||||
return 403;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /iframely/ {
|
|
||||||
proxy_pass http://iframely:80/;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# already start rust build in the background
|
|
||||||
pushd ../../server/ || exit
|
|
||||||
cargo build &
|
|
||||||
popd || exit
|
|
||||||
|
|
||||||
if [ "$1" = "-yarn" ]; then
|
|
||||||
pushd ../../ui/ || exit
|
|
||||||
yarn
|
|
||||||
yarn build
|
|
||||||
popd || exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# wait for rust build to finish
|
|
||||||
pushd ../../server/ || exit
|
|
||||||
cargo build
|
|
||||||
popd || exit
|
|
||||||
|
|
||||||
sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
|
|
||||||
|
|
||||||
for Item in alpha beta gamma ; do
|
|
||||||
sudo mkdir -p volumes/pictrs_$Item
|
|
||||||
sudo chown -R 991:991 volumes/pictrs_$Item
|
|
||||||
done
|
|
||||||
|
|
||||||
sudo docker-compose up
|
|
|
@ -1,7 +1,18 @@
|
||||||
{
|
{
|
||||||
# for more info about the config, check out the documentation
|
database: {
|
||||||
# https://dev.lemmy.ml/docs/administration_configuration.html
|
# username to connect to postgres
|
||||||
|
user: "lemmy"
|
||||||
|
# password to connect to postgres
|
||||||
|
password: "password"
|
||||||
|
# host where postgres is running
|
||||||
|
host: "postgres"
|
||||||
|
# port where postgres can be accessed
|
||||||
|
port: 5432
|
||||||
|
# name of the postgres database for lemmy
|
||||||
|
database: "lemmy"
|
||||||
|
# maximum number of active sql connections
|
||||||
|
pool_size: 5
|
||||||
|
}
|
||||||
# the domain name of your instance (eg "dev.lemmy.ml")
|
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||||
hostname: "my_domain"
|
hostname: "my_domain"
|
||||||
# address where lemmy should listen for incoming requests
|
# address where lemmy should listen for incoming requests
|
||||||
|
@ -10,19 +21,35 @@
|
||||||
port: 8536
|
port: 8536
|
||||||
# json web token for authorization between server and client
|
# json web token for authorization between server and client
|
||||||
jwt_secret: "changeme"
|
jwt_secret: "changeme"
|
||||||
# settings related to the postgresql database
|
# The dir for the front end
|
||||||
database: {
|
|
||||||
# name of the postgres database for lemmy
|
|
||||||
database: "lemmy"
|
|
||||||
# username to connect to postgres
|
|
||||||
user: "lemmy"
|
|
||||||
# password to connect to postgres
|
|
||||||
password: "password"
|
|
||||||
# host where postgres is running
|
|
||||||
host: "postgres"
|
|
||||||
}
|
|
||||||
# The location of the frontend
|
|
||||||
front_end_dir: "/app/dist"
|
front_end_dir: "/app/dist"
|
||||||
|
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
|
||||||
|
# cause problems like remote instances fetching and permanently storing bad data.
|
||||||
|
federation_enabled: false
|
||||||
|
# rate limits for various user actions, by user ip
|
||||||
|
rate_limit: {
|
||||||
|
# maximum number of messages created in interval
|
||||||
|
message: 180
|
||||||
|
# interval length for message limit
|
||||||
|
message_per_second: 60
|
||||||
|
# maximum number of posts created in interval
|
||||||
|
post: 6
|
||||||
|
# interval length for post limit
|
||||||
|
post_per_second: 600
|
||||||
|
# maximum number of registrations in interval
|
||||||
|
register: 3
|
||||||
|
# interval length for registration limit
|
||||||
|
register_per_second: 3600
|
||||||
|
}
|
||||||
|
# # optional: parameters for automatic configuration of new instance (only used at first start)
|
||||||
|
# setup: {
|
||||||
|
# # username for the admin user
|
||||||
|
# admin_username: "lemmy"
|
||||||
|
# # password for the admin user
|
||||||
|
# admin_password: "lemmy"
|
||||||
|
# # name of the site (can be changed later)
|
||||||
|
# site_name: "Lemmy Test"
|
||||||
|
# }
|
||||||
# # optional: email sending configuration
|
# # optional: email sending configuration
|
||||||
# email: {
|
# email: {
|
||||||
# # hostname of the smtp server
|
# # hostname of the smtp server
|
||||||
|
@ -31,7 +58,7 @@
|
||||||
# smtp_login: ""
|
# smtp_login: ""
|
||||||
# # password to login to the smtp server
|
# # password to login to the smtp server
|
||||||
# smtp_password: ""
|
# smtp_password: ""
|
||||||
# # address to send emails from, eg "noreply@your-instance.com"
|
# # address to send emails from, eg "info@your-instance.com"
|
||||||
# smtp_from_address: ""
|
# smtp_from_address: ""
|
||||||
# }
|
# }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
ARG RUST_BUILDER_IMAGE=shtripok/rust-musl-builder:arm
|
|
||||||
|
|
||||||
FROM $RUST_BUILDER_IMAGE as rust
|
|
||||||
|
|
||||||
#ARG RUSTRELEASEDIR="debug"
|
|
||||||
ARG RUSTRELEASEDIR="release"
|
|
||||||
|
|
||||||
# Cache deps
|
|
||||||
WORKDIR /app
|
|
||||||
RUN sudo chown -R rust:rust .
|
|
||||||
RUN USER=root cargo new server
|
|
||||||
WORKDIR /app/server
|
|
||||||
COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./
|
|
||||||
#RUN sudo chown -R rust:rust .
|
|
||||||
RUN mkdir -p ./src/bin \
|
|
||||||
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
|
|
||||||
RUN cargo build --release
|
|
||||||
RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server*
|
|
||||||
COPY --chown=rust:rust server/src ./src/
|
|
||||||
COPY --chown=rust:rust server/migrations ./migrations/
|
|
||||||
|
|
||||||
# build for release
|
|
||||||
# workaround for https://github.com/rust-lang/rust/issues/62896
|
|
||||||
RUN cargo build --frozen --release
|
|
||||||
|
|
||||||
# reduce binary size
|
|
||||||
RUN strip ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server
|
|
||||||
|
|
||||||
RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/server/
|
|
||||||
|
|
||||||
FROM $RUST_BUILDER_IMAGE as docs
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --chown=rust:rust docs ./docs
|
|
||||||
RUN mdbook build docs/
|
|
||||||
|
|
||||||
FROM node:12-buster as node
|
|
||||||
|
|
||||||
WORKDIR /app/ui
|
|
||||||
|
|
||||||
# Cache deps
|
|
||||||
COPY ui/package.json ui/yarn.lock ./
|
|
||||||
RUN yarn install --pure-lockfile --network-timeout 600000
|
|
||||||
|
|
||||||
# Build
|
|
||||||
COPY ui /app/ui
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM alpine:3.12 as lemmy
|
|
||||||
|
|
||||||
# Install libpq for postgres
|
|
||||||
RUN apk add libpq
|
|
||||||
RUN addgroup -g 1000 lemmy
|
|
||||||
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
|
|
||||||
|
|
||||||
# Copy resources
|
|
||||||
COPY --chown=lemmy:lemmy server/config/defaults.hjson /config/defaults.hjson
|
|
||||||
COPY --chown=lemmy:lemmy --from=rust /app/server/lemmy_server /app/lemmy
|
|
||||||
COPY --chown=lemmy:lemmy --from=docs /app/docs/book/ /app/dist/documentation/
|
|
||||||
COPY --chown=lemmy:lemmy --from=node /app/ui/dist /app/dist
|
|
||||||
|
|
||||||
RUN chown lemmy:lemmy /app/lemmy
|
|
||||||
USER lemmy
|
|
||||||
EXPOSE 8536
|
|
||||||
CMD ["/app/lemmy"]
|
|
|
@ -1,60 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
git checkout master
|
|
||||||
|
|
||||||
# Import translations
|
|
||||||
git fetch weblate
|
|
||||||
git merge weblate/master
|
|
||||||
|
|
||||||
# Creating the new tag
|
|
||||||
new_tag="$1"
|
|
||||||
third_semver=$(echo $new_tag | cut -d "." -f 3)
|
|
||||||
|
|
||||||
# Setting the version on the front end
|
|
||||||
cd ../../
|
|
||||||
echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
|
|
||||||
git add "ui/src/version.ts"
|
|
||||||
# Setting the version on the backend
|
|
||||||
echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
|
|
||||||
git add "server/src/version.rs"
|
|
||||||
# Setting the version for Ansible
|
|
||||||
echo $new_tag > "ansible/VERSION"
|
|
||||||
git add "ansible/VERSION"
|
|
||||||
|
|
||||||
cd docker/prod || exit
|
|
||||||
|
|
||||||
# Changing the docker-compose prod
|
|
||||||
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
|
|
||||||
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../../ansible/templates/docker-compose.yml
|
|
||||||
git add ../prod/docker-compose.yml
|
|
||||||
git add ../../ansible/templates/docker-compose.yml
|
|
||||||
|
|
||||||
# The commit
|
|
||||||
git commit -m"Version $new_tag"
|
|
||||||
git tag $new_tag
|
|
||||||
|
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=1
|
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
|
|
||||||
# Rebuilding docker
|
|
||||||
if [ $third_semver -eq 0 ]; then
|
|
||||||
# TODO get linux/arm/v7 build working
|
|
||||||
# Build for Raspberry Pi / other archs too
|
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 ../../ \
|
|
||||||
--file Dockerfile \
|
|
||||||
--tag dessalines/lemmy:$new_tag \
|
|
||||||
--push
|
|
||||||
else
|
|
||||||
docker buildx build --platform linux/amd64 ../../ \
|
|
||||||
--file Dockerfile \
|
|
||||||
--tag dessalines/lemmy:$new_tag \
|
|
||||||
--push
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Push
|
|
||||||
git push origin $new_tag
|
|
||||||
git push
|
|
||||||
|
|
||||||
# Pushing to any ansible deploys
|
|
||||||
cd ../../../lemmy-ansible || exit
|
|
||||||
ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
|
|
|
@ -1,4 +1,4 @@
|
||||||
version: '2.2'
|
version: '3.3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -12,33 +12,31 @@ services:
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.7.6
|
image: dessalines/lemmy:v0.6.44
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=error
|
- RUST_LOG=error
|
||||||
volumes:
|
volumes:
|
||||||
- ./lemmy.hjson:/config/config.hjson
|
- ./lemmy.hjson:/config/config.hjson:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- pictrs
|
- pictshare
|
||||||
- iframely
|
- iframely
|
||||||
|
|
||||||
pictrs:
|
pictshare:
|
||||||
image: asonix/pictrs:v0.1.13-r0
|
image: shtripok/pictshare:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8537:8080"
|
- "127.0.0.1:8537:80"
|
||||||
user: 991:991
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./volumes/pictrs:/mnt
|
- ./volumes/pictshare:/usr/share/nginx/html/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
iframely:
|
iframely:
|
||||||
image: jolt/iframely:v1.4.3
|
image: dogbin/iframely:latest
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8061:80"
|
- "127.0.0.1:8061:80"
|
||||||
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: 100m
|
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ $(id -u) != 0 ]]; then
|
|
||||||
echo "This migration needs to be run as root"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f docker-compose.yml ]]; then
|
|
||||||
echo "No docker-compose.yml found in current directory. Is this the right folder?"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fixing pictrs permissions
|
|
||||||
mkdir -p volumes/pictrs
|
|
||||||
sudo chown -R 991:991 volumes/pictrs
|
|
||||||
|
|
||||||
echo "Restarting docker-compose, making sure that pictrs is started and pictshare is removed"
|
|
||||||
docker-compose up -d --remove-orphans
|
|
||||||
|
|
||||||
if [[ -z $(docker-compose ps | grep pictrs) ]]; then
|
|
||||||
echo "Pict-rs is not running, make sure you update Lemmy first"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# echo "Stopping Lemmy so that users dont upload new images during the migration"
|
|
||||||
# docker-compose stop lemmy
|
|
||||||
|
|
||||||
pushd volumes/pictshare/
|
|
||||||
echo "Importing pictshare images to pict-rs..."
|
|
||||||
IMAGE_NAMES=*
|
|
||||||
for image in $IMAGE_NAMES; do
|
|
||||||
IMAGE_PATH="$(pwd)/$image/$image"
|
|
||||||
if [[ ! -f $IMAGE_PATH ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo -e "\nImporting $IMAGE_PATH"
|
|
||||||
ret=0
|
|
||||||
curl --silent --fail -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import || ret=$?
|
|
||||||
if [[ $ret != 0 ]]; then
|
|
||||||
echo "Error for $IMAGE_PATH : $ret"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Fixing permissions on pictshare folder"
|
|
||||||
find . -type d -exec chmod 755 {} \;
|
|
||||||
find . -type f -exec chmod 644 {} \;
|
|
||||||
|
|
||||||
popd
|
|
||||||
|
|
||||||
echo "Rewrite image links in Lemmy database"
|
|
||||||
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE user_ SET avatar = REPLACE(avatar, 'pictshare', 'pictrs/image') WHERE avatar is not null;"
|
|
||||||
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE post SET url = REPLACE(url, 'pictshare', 'pictrs/image') WHERE url is not null;"
|
|
||||||
|
|
||||||
echo "Moving pictshare data folder to pictshare_backup"
|
|
||||||
mv volumes/pictshare volumes/pictshare_backup
|
|
||||||
|
|
||||||
echo "Migration done, starting Lemmy again"
|
|
||||||
echo "If everything went well, you can delete ./volumes/pictshare_backup/"
|
|
||||||
docker-compose start lemmy
|
|
Binary file not shown.
Before Width: | Height: | Size: 78 KiB |
Binary file not shown.
Before Width: | Height: | Size: 92 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB |
|
@ -10,11 +10,9 @@
|
||||||
- [Install with Ansible](administration_install_ansible.md)
|
- [Install with Ansible](administration_install_ansible.md)
|
||||||
- [Install with Kubernetes](administration_install_kubernetes.md)
|
- [Install with Kubernetes](administration_install_kubernetes.md)
|
||||||
- [Configuration](administration_configuration.md)
|
- [Configuration](administration_configuration.md)
|
||||||
- [Backup and Restore](administration_backup_and_restore.md)
|
|
||||||
- [Contributing](contributing.md)
|
- [Contributing](contributing.md)
|
||||||
- [Docker Development](contributing_docker_development.md)
|
- [Docker Development](contributing_docker_development.md)
|
||||||
- [Local Development](contributing_local_development.md)
|
- [Local Development](contributing_local_development.md)
|
||||||
- [Tests](contributing_tests.md)
|
|
||||||
- [Federation Development](contributing_federation_development.md)
|
- [Federation Development](contributing_federation_development.md)
|
||||||
- [Websocket/HTTP API](contributing_websocket_http_api.md)
|
- [Websocket/HTTP API](contributing_websocket_http_api.md)
|
||||||
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
- [ActivityPub API Outline](contributing_apub_api_outline.md)
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
Front Page|Post
|
Front Page|Post
|
||||||
---|---
|
---|---
|
||||||
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
|
![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), [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).
|
[Lemmy](https://github.com/dessalines/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.
|
||||||
|
|
||||||
|
|
|
@ -51,4 +51,3 @@
|
||||||
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
|
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
|
||||||
- [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579/3)
|
- [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579/3)
|
||||||
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)
|
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)
|
||||||
- [Asonix http signatures in rust](https://git.asonix.dog/Aardwolf/http-signature-normalization)
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
Start typing...
|
Start typing...
|
||||||
|
|
||||||
- `@a_user_name` to get a list of usernames.
|
- `@a_user_name` to get a list of usernames.
|
||||||
- `!a_community` to get a list of communities.
|
- `#a_community` to get a list of communities.
|
||||||
- `:emoji` to get a list of emojis.
|
- `:emoji` to get a list of emojis.
|
||||||
|
|
||||||
## Sorting
|
## Sorting
|
||||||
|
|
|
@ -26,4 +26,4 @@ Gravity = Decay gravity, 1.8 is default
|
||||||
|
|
||||||
A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k.
|
A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k.
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/rank_algorithm.png)
|
![](https://i.imgur.com/w8oBLlL.png)
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
# Backup and Restore Guide
|
|
||||||
|
|
||||||
## Docker and Ansible
|
|
||||||
|
|
||||||
When using docker or ansible, there should be a `volumes` folder, which contains both the database, and all the pictures. Copy this folder to the new instance to restore your data.
|
|
||||||
|
|
||||||
### Incremental Database backup
|
|
||||||
|
|
||||||
To incrementally backup the DB to an `.sql` file, you can run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy > lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
|
|
||||||
```
|
|
||||||
### A Sample backup script
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/sh
|
|
||||||
# DB Backup
|
|
||||||
ssh MY_USER@MY_IP "docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy" > ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
|
|
||||||
|
|
||||||
# Volumes folder Backup
|
|
||||||
rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restoring the DB
|
|
||||||
|
|
||||||
If you need to restore from a `pg_dumpall` file, you need to first clear out your existing database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Drop the existing DB
|
|
||||||
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
|
||||||
|
|
||||||
# Restore from the .sql backup
|
|
||||||
cat db_dump.sql | docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restores the db
|
|
||||||
|
|
||||||
# This also might be necessary when doing a db import with a different password.
|
|
||||||
docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'"
|
|
||||||
```
|
|
||||||
|
|
||||||
## More resources
|
|
||||||
|
|
||||||
- https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
The configuration is based on the file
|
The configuration is based on the file [defaults.hjson](server/config/defaults.hjson). This file also contains documentation for all the available options. To override the defaults, you can copy the options you want to change into your local `config.hjson` file.
|
||||||
[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/master/server/config/defaults.hjson).
|
|
||||||
This file also contains documentation for all the available options. To override the defaults, you
|
|
||||||
can copy the options you want to change into your local `config.hjson` file.
|
|
||||||
|
|
||||||
Additionally, you can override any config files with environment variables. These have the same
|
Additionally, you can override any config files with environment variables. These have the same name as the config options, and are prefixed with `LEMMY_`. For example, you can override the `database.password` with
|
||||||
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
|
`LEMMY__DATABASE__POOL_SIZE=10`.
|
||||||
`database.password` with `LEMMY__DATABASE__POOL_SIZE=10`.
|
|
||||||
|
|
||||||
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL
|
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection details at once.
|
||||||
connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection
|
|
||||||
details at once.
|
|
||||||
|
|
||||||
If the Docker container is not used, manually create the database specified above by running the
|
If the Docker container is not used, manually create the database specified above by running the following commands:
|
||||||
following commands:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd server
|
cd server
|
||||||
|
|
|
@ -7,13 +7,10 @@ First, you need to [install Ansible on your local computer](https://docs.ansible
|
||||||
Then run the following commands on your local computer:
|
Then run the following commands on your local computer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/LemmyNet/lemmy.git
|
git clone https://github.com/dessalines/lemmy.git
|
||||||
cd lemmy/ansible/
|
cd lemmy/ansible/
|
||||||
cp inventory.example inventory
|
cp inventory.example inventory
|
||||||
nano inventory # enter your server, domain, contact email
|
nano inventory # enter your server, domain, contact email
|
||||||
# If the command below fails, you may need to comment out this line
|
|
||||||
# In the ansible.cfg file:
|
|
||||||
# interpreter_python=/usr/bin/python3
|
|
||||||
ansible-playbook lemmy.yml --become
|
ansible-playbook lemmy.yml --become
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -22,4 +19,4 @@ To update to a new version, just run the following in your local Lemmy repo:
|
||||||
git pull origin master
|
git pull origin master
|
||||||
cd ansible
|
cd ansible
|
||||||
ansible-playbook lemmy.yml --become
|
ansible-playbook lemmy.yml --become
|
||||||
```
|
```
|
|
@ -6,25 +6,19 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed. On Ubun
|
||||||
# create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want
|
# create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want
|
||||||
mkdir /lemmy
|
mkdir /lemmy
|
||||||
cd /lemmy
|
cd /lemmy
|
||||||
|
|
||||||
# download default config files
|
# download default config files
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/iframely.config.local.js
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js
|
||||||
|
docker-compose up -d
|
||||||
# Set correct permissions for pictrs folder
|
|
||||||
mkdir -p volumes/pictrs
|
|
||||||
sudo chown -R 991:991 volumes/pictrs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
After this, have a look at the [config file](administration_configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname, and possibly the db password. Then run:
|
After this, have a look at the [config file](administration_configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname.
|
||||||
|
|
||||||
`docker-compose up -d`
|
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
|
||||||
|
|
||||||
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf), could be setup with:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
|
||||||
# Replace the {{ vars }}
|
# Replace the {{ vars }}
|
||||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||||
```
|
```
|
||||||
|
@ -36,6 +30,6 @@ You will also need to setup TLS, for example with [Let's Encrypt](https://letsen
|
||||||
To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo:
|
To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
|
@ -4,14 +4,13 @@ Information about contributing to Lemmy, whether it is translating, testing, des
|
||||||
|
|
||||||
## Issue tracking / Repositories
|
## Issue tracking / Repositories
|
||||||
|
|
||||||
- [GitHub (for issues and pull requests)](https://github.com/LemmyNet/lemmy)
|
- [GitHub (for issues)](https://github.com/dessalines/lemmy)
|
||||||
- [Gitea (only for pull requests)](https://yerbamate.dev/LemmyNet/lemmy)
|
- [Gitea](https://yerbamate.dev/dessalines/lemmy)
|
||||||
- [GitLab (only code-mirror)](https://gitlab.com/dessalines/lemmy)
|
- [GitLab](https://gitlab.com/dessalines/lemmy)
|
||||||
|
|
||||||
## Translating
|
## Translating
|
||||||
|
|
||||||
Check out [Lemmy's Weblate](https://weblate.yerbamate.dev/projects/lemmy/) for translations.
|
Go [here](https://github.com/dessalines/lemmy#translations) for translation instructions.
|
||||||
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
|
@ -3,22 +3,11 @@
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install git docker-compose
|
git clone https://github.com/dessalines/lemmy
|
||||||
git clone https://github.com/LemmyNet/lemmy
|
|
||||||
cd lemmy/docker/dev
|
cd lemmy/docker/dev
|
||||||
sudo docker-compose up --no-deps --build
|
./docker_update.sh # This builds and runs it, updating for your changes
|
||||||
```
|
```
|
||||||
|
|
||||||
and go to http://localhost:8536.
|
and go to http://localhost:8536.
|
||||||
|
|
||||||
To speed up the Docker compile, add the following to `/etc/docker/daemon.json` and restart Docker.
|
Note that compile times when changing `Cargo.toml` are relatively long with Docker, because builds can't be incrementally cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md).
|
||||||
```
|
|
||||||
{
|
|
||||||
"features": {
|
|
||||||
"buildkit": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the build is still too slow, you will have to use a
|
|
||||||
[local build](contributing_local_development.md) instead.
|
|
||||||
|
|
|
@ -5,17 +5,17 @@
|
||||||
If you don't have a local clone of the Lemmy repo yet, just run the following command:
|
If you don't have a local clone of the Lemmy repo yet, just run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/LemmyNet/lemmy -b federation
|
git clone https://yerbamate.dev/nutomic/lemmy.git -b federation
|
||||||
```
|
```
|
||||||
|
|
||||||
If you already have the Lemmy repo cloned, you need to add a new remote:
|
If you already have the Lemmy repo cloned, you need to add a new remote:
|
||||||
```bash
|
```bash
|
||||||
git remote add federation https://github.com/LemmyNet/lemmy
|
git remote add federation https://yerbamate.dev/nutomic/lemmy.git
|
||||||
git checkout federation
|
git checkout federation
|
||||||
git pull federation federation
|
git pull federation federation
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running locally
|
## Running
|
||||||
|
|
||||||
You need to have the following packages installed, the Docker service needs to be running.
|
You need to have the following packages installed, the Docker service needs to be running.
|
||||||
|
|
||||||
|
@ -31,30 +31,7 @@ cd dev/federation-test
|
||||||
```
|
```
|
||||||
|
|
||||||
After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and
|
After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and
|
||||||
[127.0.0.1:8550](http://127.0.0.1:8550) in your browser to use the test instances. You can login as admin with
|
[127.0.0.1:8541](http://127.0.0.1:8541) in your browser to use the test instances. You can login as admin with
|
||||||
username `lemmy_alpha` and `lemmy_beta` respectively, with password `lemmy`.
|
username `lemmy` and password `lemmy`, or create new accounts.
|
||||||
|
|
||||||
## Running on a server
|
Please get in touch if you want to contribute to this, so we can coordinate things and avoid duplicate work.
|
||||||
|
|
||||||
Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware
|
|
||||||
that you might have to wipe the instance data at one point or another.
|
|
||||||
|
|
||||||
Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or
|
|
||||||
[manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in
|
|
||||||
`/lemmy/docker-compose.yml` with `image: dessalines/lemmy:federation`. Also add the following in
|
|
||||||
`/lemmy/lemmy.hjson`:
|
|
||||||
|
|
||||||
```
|
|
||||||
federation: {
|
|
||||||
enabled: true
|
|
||||||
allowed_instances: example.com
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Afterwards, and whenver you want to update to the latest version, run these commands on the server:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd /lemmy/
|
|
||||||
sudo docker-compose pull
|
|
||||||
sudo docker-compose up -d
|
|
||||||
```
|
|
|
@ -1,67 +1,31 @@
|
||||||
### Ubuntu
|
#### Requirements
|
||||||
|
|
||||||
|
- [Rust](https://www.rust-lang.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/en/)
|
||||||
|
- [Postgres](https://www.postgresql.org/)
|
||||||
|
|
||||||
#### Build requirements:
|
#### Set up Postgres DB
|
||||||
```
|
|
||||||
sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 git
|
|
||||||
# install yarn
|
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
|
||||||
sudo apt update && sudo apt install yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Get the source code
|
```bash
|
||||||
```
|
|
||||||
git clone https://github.com/LemmyNet/lemmy.git
|
|
||||||
# or alternatively from gitea
|
|
||||||
# git clone https://yerbamate.dev/LemmyNet/lemmy.git
|
|
||||||
```
|
|
||||||
|
|
||||||
All the following commands need to be run either in `lemmy/server` or `lemmy/ui`, as indicated
|
|
||||||
by the `cd` command.
|
|
||||||
|
|
||||||
#### Build the backend (Rust)
|
|
||||||
```
|
|
||||||
cd server
|
cd server
|
||||||
cargo build
|
./db-init.sh
|
||||||
# for development, use `cargo check` instead)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build the frontend (Typescript)
|
Or run the commands manually:
|
||||||
```
|
|
||||||
cd ui
|
|
||||||
yarn
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Setup postgresql
|
```bash
|
||||||
```
|
psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
||||||
sudo apt install postgresql
|
psql -c 'create database lemmy with owner lemmy;' -U postgres
|
||||||
sudo systemctl start postgresql
|
|
||||||
# initialize postgres database
|
|
||||||
sudo -u postgres psql -c "create user lemmy with password 'password' superuser;" -U postgres
|
|
||||||
sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -U postgres
|
|
||||||
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
# or execute server/db-init.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Run a local development instance
|
#### Running
|
||||||
```
|
|
||||||
# run each of these in a seperate terminal
|
|
||||||
cd server && cargo run
|
|
||||||
ui & yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit
|
```bash
|
||||||
any frontend files. For backend coding, you will have to rerun `cargo run`. You can use
|
git clone https://github.com/dessalines/lemmy
|
||||||
`cargo check` as a faster way to find compilation errors.
|
cd lemmy
|
||||||
|
./install.sh
|
||||||
To speed up incremental builds, you can add the following to `~/.cargo/config`:
|
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||||
|
# cd ui && yarn start
|
||||||
|
# cd server && cargo watch -x run
|
||||||
```
|
```
|
||||||
[target.x86_64-unknown-linux-gnu]
|
|
||||||
rustflags = ["-Clink-arg=-fuse-ld=lld"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that this setup doesn't include image uploads or link previews (provided by pict-rs and
|
|
||||||
iframely respectively). If you want to test those, you should use the
|
|
||||||
[Docker development](contributing_docker_development.md).
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
### Tests
|
|
||||||
|
|
||||||
#### Rust
|
|
||||||
|
|
||||||
After installing [local development dependencies](contributing_local_development.md), run the
|
|
||||||
following commands in the `server` subfolder:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql -U lemmy -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
|
||||||
export DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
|
||||||
diesel migration run
|
|
||||||
RUST_TEST_THREADS=1 cargo test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Federation
|
|
||||||
|
|
||||||
Install the [Docker development dependencies](contributing_docker_development.md), and execute
|
|
||||||
`docker/federation-test/run-tests.sh`
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Lemmy API
|
# Lemmy API
|
||||||
|
|
||||||
*Note: this may lag behind the actual API endpoints [here](../server/src/api). The API should be considered unstable and may change any time.*
|
*Note: this may lag behind the actual API endpoints [here](../server/src/api).*
|
||||||
|
|
||||||
<!-- toc -->
|
<!-- toc -->
|
||||||
|
|
||||||
|
@ -92,93 +92,85 @@
|
||||||
- [Request](#request-17)
|
- [Request](#request-17)
|
||||||
- [Response](#response-17)
|
- [Response](#response-17)
|
||||||
- [HTTP](#http-18)
|
- [HTTP](#http-18)
|
||||||
+ [Get Site Config](#get-site-config)
|
* [Community](#community)
|
||||||
|
+ [Get Community](#get-community)
|
||||||
- [Request](#request-18)
|
- [Request](#request-18)
|
||||||
- [Response](#response-18)
|
- [Response](#response-18)
|
||||||
- [HTTP](#http-19)
|
- [HTTP](#http-19)
|
||||||
+ [Save Site Config](#save-site-config)
|
+ [Create Community](#create-community)
|
||||||
- [Request](#request-19)
|
- [Request](#request-19)
|
||||||
- [Response](#response-19)
|
- [Response](#response-19)
|
||||||
- [HTTP](#http-20)
|
- [HTTP](#http-20)
|
||||||
* [Community](#community)
|
+ [List Communities](#list-communities)
|
||||||
+ [Get Community](#get-community)
|
|
||||||
- [Request](#request-20)
|
- [Request](#request-20)
|
||||||
- [Response](#response-20)
|
- [Response](#response-20)
|
||||||
- [HTTP](#http-21)
|
- [HTTP](#http-21)
|
||||||
+ [Create Community](#create-community)
|
+ [Ban from Community](#ban-from-community)
|
||||||
- [Request](#request-21)
|
- [Request](#request-21)
|
||||||
- [Response](#response-21)
|
- [Response](#response-21)
|
||||||
- [HTTP](#http-22)
|
- [HTTP](#http-22)
|
||||||
+ [List Communities](#list-communities)
|
+ [Add Mod to Community](#add-mod-to-community)
|
||||||
- [Request](#request-22)
|
- [Request](#request-22)
|
||||||
- [Response](#response-22)
|
- [Response](#response-22)
|
||||||
- [HTTP](#http-23)
|
- [HTTP](#http-23)
|
||||||
+ [Ban from Community](#ban-from-community)
|
+ [Edit Community](#edit-community)
|
||||||
- [Request](#request-23)
|
- [Request](#request-23)
|
||||||
- [Response](#response-23)
|
- [Response](#response-23)
|
||||||
- [HTTP](#http-24)
|
- [HTTP](#http-24)
|
||||||
+ [Add Mod to Community](#add-mod-to-community)
|
+ [Follow Community](#follow-community)
|
||||||
- [Request](#request-24)
|
- [Request](#request-24)
|
||||||
- [Response](#response-24)
|
- [Response](#response-24)
|
||||||
- [HTTP](#http-25)
|
- [HTTP](#http-25)
|
||||||
+ [Edit Community](#edit-community)
|
+ [Get Followed Communities](#get-followed-communities)
|
||||||
- [Request](#request-25)
|
- [Request](#request-25)
|
||||||
- [Response](#response-25)
|
- [Response](#response-25)
|
||||||
- [HTTP](#http-26)
|
- [HTTP](#http-26)
|
||||||
+ [Follow Community](#follow-community)
|
+ [Transfer Community](#transfer-community)
|
||||||
- [Request](#request-26)
|
- [Request](#request-26)
|
||||||
- [Response](#response-26)
|
- [Response](#response-26)
|
||||||
- [HTTP](#http-27)
|
- [HTTP](#http-27)
|
||||||
+ [Get Followed Communities](#get-followed-communities)
|
* [Post](#post)
|
||||||
|
+ [Create Post](#create-post)
|
||||||
- [Request](#request-27)
|
- [Request](#request-27)
|
||||||
- [Response](#response-27)
|
- [Response](#response-27)
|
||||||
- [HTTP](#http-28)
|
- [HTTP](#http-28)
|
||||||
+ [Transfer Community](#transfer-community)
|
+ [Get Post](#get-post)
|
||||||
- [Request](#request-28)
|
- [Request](#request-28)
|
||||||
- [Response](#response-28)
|
- [Response](#response-28)
|
||||||
- [HTTP](#http-29)
|
- [HTTP](#http-29)
|
||||||
* [Post](#post)
|
+ [Get Posts](#get-posts)
|
||||||
+ [Create Post](#create-post)
|
|
||||||
- [Request](#request-29)
|
- [Request](#request-29)
|
||||||
- [Response](#response-29)
|
- [Response](#response-29)
|
||||||
- [HTTP](#http-30)
|
- [HTTP](#http-30)
|
||||||
+ [Get Post](#get-post)
|
+ [Create Post Like](#create-post-like)
|
||||||
- [Request](#request-30)
|
- [Request](#request-30)
|
||||||
- [Response](#response-30)
|
- [Response](#response-30)
|
||||||
- [HTTP](#http-31)
|
- [HTTP](#http-31)
|
||||||
+ [Get Posts](#get-posts)
|
+ [Edit Post](#edit-post)
|
||||||
- [Request](#request-31)
|
- [Request](#request-31)
|
||||||
- [Response](#response-31)
|
- [Response](#response-31)
|
||||||
- [HTTP](#http-32)
|
- [HTTP](#http-32)
|
||||||
+ [Create Post Like](#create-post-like)
|
+ [Save Post](#save-post)
|
||||||
- [Request](#request-32)
|
- [Request](#request-32)
|
||||||
- [Response](#response-32)
|
- [Response](#response-32)
|
||||||
- [HTTP](#http-33)
|
- [HTTP](#http-33)
|
||||||
+ [Edit Post](#edit-post)
|
* [Comment](#comment)
|
||||||
|
+ [Create Comment](#create-comment)
|
||||||
- [Request](#request-33)
|
- [Request](#request-33)
|
||||||
- [Response](#response-33)
|
- [Response](#response-33)
|
||||||
- [HTTP](#http-34)
|
- [HTTP](#http-34)
|
||||||
+ [Save Post](#save-post)
|
+ [Edit Comment](#edit-comment)
|
||||||
- [Request](#request-34)
|
- [Request](#request-34)
|
||||||
- [Response](#response-34)
|
- [Response](#response-34)
|
||||||
- [HTTP](#http-35)
|
- [HTTP](#http-35)
|
||||||
* [Comment](#comment)
|
+ [Save Comment](#save-comment)
|
||||||
+ [Create Comment](#create-comment)
|
|
||||||
- [Request](#request-35)
|
- [Request](#request-35)
|
||||||
- [Response](#response-35)
|
- [Response](#response-35)
|
||||||
- [HTTP](#http-36)
|
- [HTTP](#http-36)
|
||||||
+ [Edit Comment](#edit-comment)
|
+ [Create Comment Like](#create-comment-like)
|
||||||
- [Request](#request-36)
|
- [Request](#request-36)
|
||||||
- [Response](#response-36)
|
- [Response](#response-36)
|
||||||
- [HTTP](#http-37)
|
- [HTTP](#http-37)
|
||||||
+ [Save Comment](#save-comment)
|
|
||||||
- [Request](#request-37)
|
|
||||||
- [Response](#response-37)
|
|
||||||
- [HTTP](#http-38)
|
|
||||||
+ [Create Comment Like](#create-comment-like)
|
|
||||||
- [Request](#request-38)
|
|
||||||
- [Response](#response-38)
|
|
||||||
- [HTTP](#http-39)
|
|
||||||
* [RSS / Atom feeds](#rss--atom-feeds)
|
* [RSS / Atom feeds](#rss--atom-feeds)
|
||||||
+ [All](#all)
|
+ [All](#all)
|
||||||
+ [Community](#community-1)
|
+ [Community](#community-1)
|
||||||
|
@ -787,53 +779,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
|
|
||||||
`POST /site/transfer`
|
`POST /site/transfer`
|
||||||
|
|
||||||
#### Get Site Config
|
|
||||||
##### Request
|
|
||||||
```rust
|
|
||||||
{
|
|
||||||
op: "GetSiteConfig",
|
|
||||||
data: {
|
|
||||||
auth: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### Response
|
|
||||||
```rust
|
|
||||||
{
|
|
||||||
op: "GetSiteConfig",
|
|
||||||
data: {
|
|
||||||
config_hjson: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### HTTP
|
|
||||||
|
|
||||||
`GET /site/config`
|
|
||||||
|
|
||||||
#### Save Site Config
|
|
||||||
##### Request
|
|
||||||
```rust
|
|
||||||
{
|
|
||||||
op: "SaveSiteConfig",
|
|
||||||
data: {
|
|
||||||
config_hjson: String,
|
|
||||||
auth: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### Response
|
|
||||||
```rust
|
|
||||||
{
|
|
||||||
op: "SaveSiteConfig",
|
|
||||||
data: {
|
|
||||||
config_hjson: String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### HTTP
|
|
||||||
|
|
||||||
`PUT /site/config`
|
|
||||||
|
|
||||||
### Community
|
### Community
|
||||||
#### Get Community
|
#### Get Community
|
||||||
##### Request
|
##### Request
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Lemmy Council
|
# Lemmy Council
|
||||||
|
|
||||||
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
|
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
|
||||||
- Council members are also added as administrators to any official Lemmy instances.
|
|
||||||
|
|
||||||
## Voting / Decision-Making
|
## Voting / Decision-Making
|
||||||
|
|
||||||
|
@ -50,7 +49,5 @@
|
||||||
## Member List / Contact Info
|
## Member List / Contact Info
|
||||||
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
|
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
|
||||||
|
|
||||||
- [Dessalines](https://dev.lemmy.ml/u/dessalines)
|
- Dessalines [Matrix](https://matrix.to/#/@happydooby:matrix.org)
|
||||||
- [Nutomic](https://dev.lemmy.ml/u/nutomic)
|
- Nutomic [Matrix](https://matrix.to/#/@nutomic:matrix.org), [Mastodon](https://radical.town/@felix)
|
||||||
- [AgreeableLandscape](https://dev.lemmy.ml/u/AgreeableLandscape)
|
|
||||||
- [fruechtchen](https://dev.lemmy.ml/u/fruechtchen)
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Set the database variable to the default first.
|
# Set the database variable to the default first.
|
||||||
|
@ -10,55 +10,25 @@ export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
|
||||||
export JWT_SECRET=changeme
|
export JWT_SECRET=changeme
|
||||||
export HOSTNAME=rrr
|
export HOSTNAME=rrr
|
||||||
|
|
||||||
yes_no_prompt_invalid() {
|
|
||||||
echo "Invalid input. Please enter either \"y\" or \"n\"." 1>&2
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_to_init_db() {
|
|
||||||
init_db_valid=0
|
|
||||||
init_db_final=0
|
|
||||||
while [ "$init_db_valid" == 0 ]
|
|
||||||
do
|
|
||||||
read -p "Initialize database (y/n)? " init_db
|
|
||||||
case "$init_db" in
|
|
||||||
[yY]* ) init_db_valid=1; init_db_final=1;;
|
|
||||||
[nN]* ) init_db_valid=1; init_db_final=0;;
|
|
||||||
* ) yes_no_prompt_invalid;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
if [ "$init_db_final" = 1 ]
|
|
||||||
then
|
|
||||||
source ./server/db-init.sh
|
|
||||||
read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..."
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_to_auto_reload() {
|
|
||||||
auto_reload_valid=0
|
|
||||||
auto_reload_final=0
|
|
||||||
while [ "$auto_reload_valid" == 0 ]
|
|
||||||
do
|
|
||||||
echo "Automagically reload the project when source files are changed?"
|
|
||||||
echo "ONLY ENABLE THIS FOR DEVELOPMENT!"
|
|
||||||
read -p "(y/n) " auto_reload
|
|
||||||
case "$auto_reload" in
|
|
||||||
[yY]* ) auto_reload_valid=1; auto_reload_final=1;;
|
|
||||||
[nN]* ) auto_reload_valid=1; auto_reload_final=0;;
|
|
||||||
* ) yes_no_prompt_invalid;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
if [ "$auto_reload_final" = 1 ]
|
|
||||||
then
|
|
||||||
cd ui && yarn start
|
|
||||||
cd server && cargo watch -x run
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optionally initialize the database
|
# Optionally initialize the database
|
||||||
ask_to_init_db
|
init_db_valid=0
|
||||||
|
init_db_final=0
|
||||||
|
while [ "$init_db_valid" == 0 ]
|
||||||
|
do
|
||||||
|
read -p "Initialize database (y/n)? " init_db
|
||||||
|
case "${init_db,,}" in
|
||||||
|
y|yes ) init_db_valid=1; init_db_final=1;;
|
||||||
|
n|no ) init_db_valid=1; init_db_final=0;;
|
||||||
|
* ) echo "Invalid input" 1>&2;;
|
||||||
|
esac
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
if [ "$init_db_final" = 1 ]
|
||||||
|
then
|
||||||
|
source ./server/db-init.sh
|
||||||
|
read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
# Build the web client
|
# Build the web client
|
||||||
cd ui
|
cd ui
|
||||||
|
@ -69,5 +39,6 @@ yarn build
|
||||||
cd ../server
|
cd ../server
|
||||||
RUST_LOG=debug cargo run
|
RUST_LOG=debug cargo run
|
||||||
|
|
||||||
# For live coding, where both the front and back end, automagically reload on any save
|
# For live coding, where both the front and back end, automagically reload on any save, do:
|
||||||
ask_to_auto_reload
|
# cd ui && yarn start
|
||||||
|
# cd server && cargo watch -x run
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
edition="2018"
|
edition="2018"
|
||||||
imports_layout="HorizontalVertical"
|
|
||||||
merge_imports=true
|
|
||||||
reorder_imports=true
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,30 +1,24 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lemmy_server"
|
name = "lemmy_server"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
authors = ["Dessalines <tyhou13@gmx.com>"]
|
authors = ["Dessalines <happydooby@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
|
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
|
||||||
diesel_migrations = "1.4.0"
|
diesel_migrations = "1.4.0"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
activitystreams = "0.6.2"
|
activitystreams = "0.5.0-alpha.16"
|
||||||
activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" }
|
bcrypt = "0.6.2"
|
||||||
activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
|
|
||||||
bcrypt = "0.8.0"
|
|
||||||
chrono = { version = "0.4.7", features = ["serde"] }
|
chrono = { version = "0.4.7", features = ["serde"] }
|
||||||
serde_json = { version = "1.0.52", features = ["preserve_order"]}
|
failure = "0.1.5"
|
||||||
failure = "0.1.8"
|
serde_json = { version = "1.0.48", features = ["preserve_order"]}
|
||||||
serde = { version = "1.0.105", features = ["derive"] }
|
serde = { version = "1.0.105", features = ["derive"] }
|
||||||
actix = "0.10.0-alpha.2"
|
actix = "0.9.0"
|
||||||
actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] }
|
actix-web = "2.0.0"
|
||||||
actix-files = "0.3.0-alpha.1"
|
actix-files = "0.2.1"
|
||||||
actix-web-actors = "3.0.0-alpha.1"
|
actix-web-actors = "2.0.0"
|
||||||
actix-rt = "1.1.1"
|
actix-rt = "1.0.0"
|
||||||
awc = "2.0.0-alpha.2"
|
|
||||||
log = "0.4.0"
|
log = "0.4.0"
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.7.1"
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
|
@ -33,21 +27,15 @@ strum_macros = "0.18.0"
|
||||||
jsonwebtoken = "7.0.1"
|
jsonwebtoken = "7.0.1"
|
||||||
regex = "1.3.5"
|
regex = "1.3.5"
|
||||||
lazy_static = "1.3.0"
|
lazy_static = "1.3.0"
|
||||||
lettre = "0.9.3"
|
lettre = "0.9.2"
|
||||||
lettre_email = "0.9.4"
|
lettre_email = "0.9.2"
|
||||||
|
sha2 = "0.8.1"
|
||||||
rss = "1.9.0"
|
rss = "1.9.0"
|
||||||
htmlescape = "0.3.1"
|
htmlescape = "0.3.1"
|
||||||
url = { version = "2.1.1", features = ["serde"] }
|
config = "0.10.1"
|
||||||
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
|
hjson = "0.8.2"
|
||||||
|
url = "2.1.1"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
|
isahc = "0.9"
|
||||||
comrak = "0.7"
|
comrak = "0.7"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
http = "0.2.1"
|
|
||||||
http-signature-normalization-actix = { version = "0.4.0-alpha.0", default-features = false, features = ["sha-2"] }
|
|
||||||
base64 = "0.12.1"
|
|
||||||
tokio = "0.2.21"
|
|
||||||
futures = "0.3.5"
|
|
||||||
itertools = "0.9.0"
|
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
|
||||||
sha2 = "0.9"
|
|
||||||
async-trait = "0.1.36"
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
port: 8536
|
port: 8536
|
||||||
# json web token for authorization between server and client
|
# json web token for authorization between server and client
|
||||||
jwt_secret: "changeme"
|
jwt_secret: "changeme"
|
||||||
# The location of the frontend
|
# The dir for the front end
|
||||||
front_end_dir: "../ui/dist"
|
front_end_dir: "../ui/dist"
|
||||||
# rate limits for various user actions, by user ip
|
# rate limits for various user actions, by user ip
|
||||||
rate_limit: {
|
rate_limit: {
|
||||||
|
@ -54,22 +54,20 @@
|
||||||
federation: {
|
federation: {
|
||||||
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
|
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# comma seperated list of instances to follow
|
||||||
|
followed_instances: ""
|
||||||
# whether tls is required for activitypub. only disable this for debugging, never for producion.
|
# whether tls is required for activitypub. only disable this for debugging, never for producion.
|
||||||
tls_enabled: true
|
tls_enabled: true
|
||||||
# comma seperated list of instances with which federation is allowed
|
|
||||||
allowed_instances: ""
|
|
||||||
}
|
}
|
||||||
# # email sending configuration
|
# # email sending configuration
|
||||||
# email: {
|
# email: {
|
||||||
# # hostname and port of the smtp server
|
# # hostname of the smtp server
|
||||||
# smtp_server: ""
|
# smtp_server: ""
|
||||||
# # login name for smtp server
|
# # login name for smtp server
|
||||||
# smtp_login: ""
|
# smtp_login: ""
|
||||||
# # password to login to the smtp server
|
# # password to login to the smtp server
|
||||||
# smtp_password: ""
|
# smtp_password: ""
|
||||||
# # address to send emails from, eg "noreply@your-instance.com"
|
# # address to send emails from, eg "info@your-instance.com"
|
||||||
# smtp_from_address: ""
|
# smtp_from_address: ""
|
||||||
# # whether or not smtp connections should use tls
|
|
||||||
# use_tls: true
|
|
||||||
# }
|
# }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,106 +1,43 @@
|
||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
|
|
||||||
# Default configurations
|
|
||||||
username=lemmy
|
username=lemmy
|
||||||
dbname=lemmy
|
dbname=lemmy
|
||||||
port=5432
|
port=5432
|
||||||
|
|
||||||
yes_no_prompt_invalid() {
|
password=""
|
||||||
echo "Invalid input. Please enter either \"y\" or \"n\"." 1>&2
|
password_confirm=""
|
||||||
}
|
password_valid=0
|
||||||
|
|
||||||
print_config() {
|
while [ "$password_valid" == 0 ]
|
||||||
echo " database name: $dbname"
|
do
|
||||||
echo " username: $username"
|
read -p "Enter database password: " -s password
|
||||||
echo " port: $port"
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_for_db_config() {
|
|
||||||
echo "The default database configuration is:"
|
|
||||||
print_config
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
default_config_final=0
|
read -p "Verify database password: " -s password_confirm
|
||||||
default_config_valid=0
|
echo
|
||||||
while [ "$default_config_valid" == 0 ]
|
echo
|
||||||
do
|
|
||||||
read -p "Use this configuration (y/n)? " default_config
|
|
||||||
case "$default_config" in
|
|
||||||
[yY]* ) default_config_valid=1; default_config_final=1;;
|
|
||||||
[nN]* ) default_config_valid=1; default_config_final=0;;
|
|
||||||
* ) yes_no_prompt_invalid;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$default_config_final" == 0 ]
|
# Start the loop from the top if either check fails
|
||||||
|
if [ -z "$password" ]
|
||||||
then
|
then
|
||||||
config_ok_final=0
|
echo "Error: Password cannot be empty." 1>&2
|
||||||
while [ "$config_ok_final" == 0 ]
|
echo
|
||||||
do
|
continue
|
||||||
read -p "Database name: " dbname
|
|
||||||
read -p "Username: " username
|
|
||||||
read -p "Port: " port
|
|
||||||
#echo
|
|
||||||
|
|
||||||
#echo "The database configuration is:"
|
|
||||||
#print_config
|
|
||||||
#echo
|
|
||||||
|
|
||||||
config_ok_valid=0
|
|
||||||
while [ "$config_ok_valid" == 0 ]
|
|
||||||
do
|
|
||||||
read -p "Use this configuration (y/n)? " config_ok
|
|
||||||
case "$config_ok" in
|
|
||||||
[yY]* ) config_ok_valid=1; config_ok_final=1;;
|
|
||||||
[nN]* ) config_ok_valid=1; config_ok_final=0;;
|
|
||||||
* ) yes_no_prompt_invalid;;
|
|
||||||
esac
|
|
||||||
echo
|
|
||||||
done
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
}
|
if [ "$password" != "$password_confirm" ]
|
||||||
|
then
|
||||||
ask_for_password() {
|
echo "Error: Passwords don't match." 1>&2
|
||||||
password=""
|
|
||||||
password_confirm=""
|
|
||||||
password_valid=0
|
|
||||||
while [ "$password_valid" == 0 ]
|
|
||||||
do
|
|
||||||
read -p "Enter database password: " -s password
|
|
||||||
echo
|
echo
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
read -p "Verify database password: " -s password_confirm
|
# Set the password_valid variable to break out of the loop
|
||||||
echo
|
password_valid=1
|
||||||
echo
|
done
|
||||||
|
|
||||||
# Start the loop from the top if either check fails
|
|
||||||
if [ -z "$password" ]
|
|
||||||
then
|
|
||||||
echo "Error: Password cannot be empty." 1>&2
|
|
||||||
echo
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [ "$password" != "$password_confirm" ]
|
|
||||||
then
|
|
||||||
echo "Error: Passwords don't match." 1>&2
|
|
||||||
echo
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set the password_valid variable to break out of the loop
|
|
||||||
password_valid=1
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_for_db_config
|
|
||||||
|
|
||||||
ask_for_password
|
|
||||||
|
|
||||||
psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres
|
psql -c "CREATE USER $username WITH PASSWORD '$password' SUPERUSER;" -U postgres
|
||||||
psql -c "CREATE DATABASE $dbname WITH OWNER $username;" -U postgres
|
psql -c 'CREATE DATABASE $dbname WITH OWNER $username;' -U postgres
|
||||||
export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname
|
export LEMMY_DATABASE_URL=postgres://$username:$password@localhost:$port/$dbname
|
||||||
|
|
||||||
echo "The database URL is $LEMMY_DATABASE_URL"
|
echo $LEMMY_DATABASE_URL
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ create unique index idx_activity_unique_apid on activity ((data ->> 'id'::text))
|
||||||
-- Add federation columns to the two actor tables
|
-- Add federation columns to the two actor tables
|
||||||
alter table user_
|
alter table user_
|
||||||
-- TODO uniqueness constraints should be added on these 3 columns later
|
-- TODO uniqueness constraints should be added on these 3 columns later
|
||||||
add column actor_id character varying(255) not null default 'http://fake.com', -- This needs to be checked and updated in code, building from the site url if local
|
add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
|
||||||
add column bio text, -- not on community, already has description
|
add column bio text, -- not on community, already has description
|
||||||
add column local boolean not null default true,
|
add column local boolean not null default true,
|
||||||
add column private_key text, -- These need to be generated from code
|
add column private_key text, -- These need to be generated from code
|
||||||
|
@ -25,7 +25,7 @@ add column last_refreshed_at timestamp not null default now() -- Used to re-fetc
|
||||||
|
|
||||||
-- Community
|
-- Community
|
||||||
alter table community
|
alter table community
|
||||||
add column actor_id character varying(255) not null default 'http://fake.com', -- This needs to be checked and updated in code, building from the site url if local
|
add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
|
||||||
add column local boolean not null default true,
|
add column local boolean not null default true,
|
||||||
add column private_key text, -- These need to be generated from code
|
add column private_key text, -- These need to be generated from code
|
||||||
add column public_key text,
|
add column public_key text,
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
alter table post
|
alter table post
|
||||||
-- TODO uniqueness constraints should be added on these 3 columns later
|
-- TODO uniqueness constraints should be added on these 3 columns later
|
||||||
add column ap_id character varying(255) not null default 'http://fake.com', -- This needs to be checked and updated in code, building from the site url if local
|
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
|
||||||
add column local boolean not null default true
|
add column local boolean not null default true
|
||||||
;
|
;
|
||||||
|
|
||||||
alter table comment
|
alter table comment
|
||||||
-- TODO uniqueness constraints should be added on these 3 columns later
|
-- TODO uniqueness constraints should be added on these 3 columns later
|
||||||
add column ap_id character varying(255) not null default 'http://fake.com', -- This needs to be checked and updated in code, building from the site url if local
|
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
|
||||||
add column local boolean not null default true
|
add column local boolean not null default true
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
drop view user_view cascade;
|
drop view user_view cascade;
|
||||||
|
|
||||||
alter table user_
|
alter table user_
|
||||||
add column fedi_name varchar(40) not null default 'http://fake.com';
|
add column fedi_name varchar(40) not null default 'changeme';
|
||||||
|
|
||||||
alter table user_
|
alter table user_
|
||||||
add constraint user__name_fedi_name_key unique (name, fedi_name);
|
add constraint user__name_fedi_name_key unique (name, fedi_name);
|
||||||
|
|
|
@ -1,440 +0,0 @@
|
||||||
-- user_view
|
|
||||||
drop view user_view cascade;
|
|
||||||
|
|
||||||
create view user_view as
|
|
||||||
select
|
|
||||||
u.id,
|
|
||||||
u.name,
|
|
||||||
u.avatar,
|
|
||||||
u.email,
|
|
||||||
u.matrix_user_id,
|
|
||||||
u.admin,
|
|
||||||
u.banned,
|
|
||||||
u.show_avatars,
|
|
||||||
u.send_notifications_to_email,
|
|
||||||
u.published,
|
|
||||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
|
||||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
|
||||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
|
||||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
|
||||||
from user_ u;
|
|
||||||
|
|
||||||
create materialized view user_mview as select * from user_view;
|
|
||||||
|
|
||||||
create unique index idx_user_mview_id on user_mview (id);
|
|
||||||
|
|
||||||
-- community_view
|
|
||||||
drop view community_aggregates_view cascade;
|
|
||||||
create view community_aggregates_view as
|
|
||||||
select c.*,
|
|
||||||
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
|
||||||
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
|
|
||||||
(select name from category ct where c.category_id = ct.id) as category_name,
|
|
||||||
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
|
||||||
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
|
||||||
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
|
||||||
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
|
||||||
from community c;
|
|
||||||
|
|
||||||
create materialized view community_aggregates_mview as select * from community_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view community_view as
|
|
||||||
with all_community as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from community_aggregates_view ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
|
||||||
from user_ u
|
|
||||||
cross join all_community ac
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as subscribed
|
|
||||||
from all_community ac
|
|
||||||
;
|
|
||||||
|
|
||||||
create view community_mview as
|
|
||||||
with all_community as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from community_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
|
||||||
from user_ u
|
|
||||||
cross join all_community ac
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as subscribed
|
|
||||||
from all_community ac
|
|
||||||
;
|
|
||||||
|
|
||||||
-- community views
|
|
||||||
drop view community_moderator_view;
|
|
||||||
drop view community_follower_view;
|
|
||||||
drop view community_user_ban_view;
|
|
||||||
|
|
||||||
create view community_moderator_view as
|
|
||||||
select *,
|
|
||||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cm.user_id = u.id),
|
|
||||||
(select name from community c where cm.community_id = c.id) as community_name
|
|
||||||
from community_moderator cm;
|
|
||||||
|
|
||||||
create view community_follower_view as
|
|
||||||
select *,
|
|
||||||
(select name from user_ u where cf.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cf.user_id = u.id),
|
|
||||||
(select name from community c where cf.community_id = c.id) as community_name
|
|
||||||
from community_follower cf;
|
|
||||||
|
|
||||||
create view community_user_ban_view as
|
|
||||||
select *,
|
|
||||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cm.user_id = u.id),
|
|
||||||
(select name from community c where cm.community_id = c.id) as community_name
|
|
||||||
from community_user_ban cm;
|
|
||||||
|
|
||||||
-- post_view
|
|
||||||
drop view post_view;
|
|
||||||
drop view post_mview;
|
|
||||||
drop materialized view post_aggregates_mview;
|
|
||||||
drop view post_aggregates_view;
|
|
||||||
|
|
||||||
-- regen post view
|
|
||||||
create view post_aggregates_view as
|
|
||||||
select
|
|
||||||
p.*,
|
|
||||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
|
||||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
|
||||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
|
||||||
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
|
||||||
(select name from community where p.community_id = community.id) as community_name,
|
|
||||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
|
||||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
|
||||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
|
||||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
|
||||||
coalesce(sum(pl.score), 0) as score,
|
|
||||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
|
||||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
|
||||||
hot_rank(coalesce(sum(pl.score) , 0),
|
|
||||||
(
|
|
||||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
|
||||||
else greatest(c.recent_comment_time, p.published)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
) as hot_rank,
|
|
||||||
(
|
|
||||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
|
||||||
else greatest(c.recent_comment_time, p.published)
|
|
||||||
end
|
|
||||||
) as newest_activity_time
|
|
||||||
from post p
|
|
||||||
left join post_like pl on p.id = pl.post_id
|
|
||||||
left join (
|
|
||||||
select post_id,
|
|
||||||
max(published) as recent_comment_time
|
|
||||||
from comment
|
|
||||||
group by 1
|
|
||||||
) c on p.id = c.post_id
|
|
||||||
group by p.id, c.recent_comment_time;
|
|
||||||
|
|
||||||
create materialized view post_aggregates_mview as select * from post_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view post_view as
|
|
||||||
with all_post as (
|
|
||||||
select
|
|
||||||
pa.*
|
|
||||||
from post_aggregates_view pa
|
|
||||||
)
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(pl.score, 0) as my_vote,
|
|
||||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
|
||||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
|
||||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_post ap
|
|
||||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as read,
|
|
||||||
null as saved
|
|
||||||
from all_post ap
|
|
||||||
;
|
|
||||||
|
|
||||||
create view post_mview as
|
|
||||||
with all_post as (
|
|
||||||
select
|
|
||||||
pa.*
|
|
||||||
from post_aggregates_mview pa
|
|
||||||
)
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(pl.score, 0) as my_vote,
|
|
||||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
|
||||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
|
||||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_post ap
|
|
||||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as read,
|
|
||||||
null as saved
|
|
||||||
from all_post ap
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
-- reply_view, comment_view, user_mention
|
|
||||||
drop view reply_view;
|
|
||||||
drop view user_mention_view;
|
|
||||||
drop view user_mention_mview;
|
|
||||||
drop view comment_view;
|
|
||||||
drop view comment_mview;
|
|
||||||
drop materialized view comment_aggregates_mview;
|
|
||||||
drop view comment_aggregates_view;
|
|
||||||
|
|
||||||
-- reply and comment view
|
|
||||||
create view comment_aggregates_view as
|
|
||||||
select
|
|
||||||
c.*,
|
|
||||||
(select community_id from post p where p.id = c.post_id),
|
|
||||||
(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
|
|
||||||
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
|
||||||
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
|
||||||
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
|
||||||
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
|
|
||||||
coalesce(sum(cl.score), 0) as score,
|
|
||||||
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
|
||||||
count (case when cl.score = -1 then 1 else null end) as downvotes,
|
|
||||||
hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
|
|
||||||
from comment c
|
|
||||||
left join comment_like cl on c.id = cl.comment_id
|
|
||||||
group by c.id;
|
|
||||||
|
|
||||||
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view comment_view as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_view ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as saved
|
|
||||||
from all_comment ac
|
|
||||||
;
|
|
||||||
|
|
||||||
create view comment_mview as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as saved
|
|
||||||
from all_comment ac
|
|
||||||
;
|
|
||||||
|
|
||||||
-- Do the reply_view referencing the comment_mview
|
|
||||||
create view reply_view as
|
|
||||||
with closereply as (
|
|
||||||
select
|
|
||||||
c2.id,
|
|
||||||
c2.creator_id as sender_id,
|
|
||||||
c.creator_id as recipient_id
|
|
||||||
from comment c
|
|
||||||
inner join comment c2 on c.id = c2.parent_id
|
|
||||||
where c2.creator_id != c.creator_id
|
|
||||||
-- Do union where post is null
|
|
||||||
union
|
|
||||||
select
|
|
||||||
c.id,
|
|
||||||
c.creator_id as sender_id,
|
|
||||||
p.creator_id as recipient_id
|
|
||||||
from comment c, post p
|
|
||||||
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
|
||||||
)
|
|
||||||
select cv.*,
|
|
||||||
closereply.recipient_id
|
|
||||||
from comment_mview cv, closereply
|
|
||||||
where closereply.id = cv.id
|
|
||||||
;
|
|
||||||
|
|
||||||
-- user mention
|
|
||||||
create view user_mention_view as
|
|
||||||
select
|
|
||||||
c.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
c.creator_id,
|
|
||||||
c.post_id,
|
|
||||||
c.parent_id,
|
|
||||||
c.content,
|
|
||||||
c.removed,
|
|
||||||
um.read,
|
|
||||||
c.published,
|
|
||||||
c.updated,
|
|
||||||
c.deleted,
|
|
||||||
c.community_id,
|
|
||||||
c.community_name,
|
|
||||||
c.banned,
|
|
||||||
c.banned_from_community,
|
|
||||||
c.creator_name,
|
|
||||||
c.creator_avatar,
|
|
||||||
c.score,
|
|
||||||
c.upvotes,
|
|
||||||
c.downvotes,
|
|
||||||
c.hot_rank,
|
|
||||||
c.user_id,
|
|
||||||
c.my_vote,
|
|
||||||
c.saved,
|
|
||||||
um.recipient_id
|
|
||||||
from user_mention um, comment_view c
|
|
||||||
where um.comment_id = c.id;
|
|
||||||
|
|
||||||
|
|
||||||
create view user_mention_mview as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
ac.creator_id,
|
|
||||||
ac.post_id,
|
|
||||||
ac.parent_id,
|
|
||||||
ac.content,
|
|
||||||
ac.removed,
|
|
||||||
um.read,
|
|
||||||
ac.published,
|
|
||||||
ac.updated,
|
|
||||||
ac.deleted,
|
|
||||||
ac.community_id,
|
|
||||||
ac.community_name,
|
|
||||||
ac.banned,
|
|
||||||
ac.banned_from_community,
|
|
||||||
ac.creator_name,
|
|
||||||
ac.creator_avatar,
|
|
||||||
ac.score,
|
|
||||||
ac.upvotes,
|
|
||||||
ac.downvotes,
|
|
||||||
ac.hot_rank,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
|
|
||||||
um.recipient_id
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
left join user_mention um on um.comment_id = ac.id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
ac.creator_id,
|
|
||||||
ac.post_id,
|
|
||||||
ac.parent_id,
|
|
||||||
ac.content,
|
|
||||||
ac.removed,
|
|
||||||
um.read,
|
|
||||||
ac.published,
|
|
||||||
ac.updated,
|
|
||||||
ac.deleted,
|
|
||||||
ac.community_id,
|
|
||||||
ac.community_name,
|
|
||||||
ac.banned,
|
|
||||||
ac.banned_from_community,
|
|
||||||
ac.creator_name,
|
|
||||||
ac.creator_avatar,
|
|
||||||
ac.score,
|
|
||||||
ac.upvotes,
|
|
||||||
ac.downvotes,
|
|
||||||
ac.hot_rank,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as saved,
|
|
||||||
um.recipient_id
|
|
||||||
from all_comment ac
|
|
||||||
left join user_mention um on um.comment_id = ac.id
|
|
||||||
;
|
|
||||||
|
|
|
@ -1,497 +0,0 @@
|
||||||
-- user_view
|
|
||||||
drop view user_view cascade;
|
|
||||||
|
|
||||||
create view user_view as
|
|
||||||
select
|
|
||||||
u.id,
|
|
||||||
u.actor_id,
|
|
||||||
u.name,
|
|
||||||
u.avatar,
|
|
||||||
u.email,
|
|
||||||
u.matrix_user_id,
|
|
||||||
u.bio,
|
|
||||||
u.local,
|
|
||||||
u.admin,
|
|
||||||
u.banned,
|
|
||||||
u.show_avatars,
|
|
||||||
u.send_notifications_to_email,
|
|
||||||
u.published,
|
|
||||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
|
||||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
|
||||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
|
||||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
|
||||||
from user_ u;
|
|
||||||
|
|
||||||
create materialized view user_mview as select * from user_view;
|
|
||||||
|
|
||||||
create unique index idx_user_mview_id on user_mview (id);
|
|
||||||
|
|
||||||
-- community_view
|
|
||||||
drop view community_aggregates_view cascade;
|
|
||||||
create view community_aggregates_view as
|
|
||||||
-- Now that there's public and private keys, you have to be explicit here
|
|
||||||
select c.id,
|
|
||||||
c.name,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.category_id,
|
|
||||||
c.creator_id,
|
|
||||||
c.removed,
|
|
||||||
c.published,
|
|
||||||
c.updated,
|
|
||||||
c.deleted,
|
|
||||||
c.nsfw,
|
|
||||||
c.actor_id,
|
|
||||||
c.local,
|
|
||||||
c.last_refreshed_at,
|
|
||||||
(select actor_id from user_ u where c.creator_id = u.id) as creator_actor_id,
|
|
||||||
(select local from user_ u where c.creator_id = u.id) as creator_local,
|
|
||||||
(select name from user_ u where c.creator_id = u.id) as creator_name,
|
|
||||||
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
|
|
||||||
(select name from category ct where c.category_id = ct.id) as category_name,
|
|
||||||
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
|
|
||||||
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
|
|
||||||
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
|
|
||||||
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
|
|
||||||
from community c;
|
|
||||||
|
|
||||||
create materialized view community_aggregates_mview as select * from community_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view community_view as
|
|
||||||
with all_community as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from community_aggregates_view ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
|
||||||
from user_ u
|
|
||||||
cross join all_community ac
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as subscribed
|
|
||||||
from all_community ac
|
|
||||||
;
|
|
||||||
|
|
||||||
create view community_mview as
|
|
||||||
with all_community as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from community_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
|
|
||||||
from user_ u
|
|
||||||
cross join all_community ac
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as subscribed
|
|
||||||
from all_community ac
|
|
||||||
;
|
|
||||||
|
|
||||||
-- community views
|
|
||||||
drop view community_moderator_view;
|
|
||||||
drop view community_follower_view;
|
|
||||||
drop view community_user_ban_view;
|
|
||||||
|
|
||||||
create view community_moderator_view as
|
|
||||||
select *,
|
|
||||||
(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
|
|
||||||
(select local from user_ u where cm.user_id = u.id) as user_local,
|
|
||||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cm.user_id = u.id),
|
|
||||||
(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
|
|
||||||
(select local from community c where cm.community_id = c.id) as community_local,
|
|
||||||
(select name from community c where cm.community_id = c.id) as community_name
|
|
||||||
from community_moderator cm;
|
|
||||||
|
|
||||||
create view community_follower_view as
|
|
||||||
select *,
|
|
||||||
(select actor_id from user_ u where cf.user_id = u.id) as user_actor_id,
|
|
||||||
(select local from user_ u where cf.user_id = u.id) as user_local,
|
|
||||||
(select name from user_ u where cf.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cf.user_id = u.id),
|
|
||||||
(select actor_id from community c where cf.community_id = c.id) as community_actor_id,
|
|
||||||
(select local from community c where cf.community_id = c.id) as community_local,
|
|
||||||
(select name from community c where cf.community_id = c.id) as community_name
|
|
||||||
from community_follower cf;
|
|
||||||
|
|
||||||
create view community_user_ban_view as
|
|
||||||
select *,
|
|
||||||
(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
|
|
||||||
(select local from user_ u where cm.user_id = u.id) as user_local,
|
|
||||||
(select name from user_ u where cm.user_id = u.id) as user_name,
|
|
||||||
(select avatar from user_ u where cm.user_id = u.id),
|
|
||||||
(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
|
|
||||||
(select local from community c where cm.community_id = c.id) as community_local,
|
|
||||||
(select name from community c where cm.community_id = c.id) as community_name
|
|
||||||
from community_user_ban cm;
|
|
||||||
|
|
||||||
-- post_view
|
|
||||||
drop view post_view;
|
|
||||||
drop view post_mview;
|
|
||||||
drop materialized view post_aggregates_mview;
|
|
||||||
drop view post_aggregates_view;
|
|
||||||
|
|
||||||
-- regen post view
|
|
||||||
create view post_aggregates_view as
|
|
||||||
select
|
|
||||||
p.*,
|
|
||||||
(select u.banned from user_ u where p.creator_id = u.id) as banned,
|
|
||||||
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
|
|
||||||
(select actor_id from user_ where p.creator_id = user_.id) as creator_actor_id,
|
|
||||||
(select local from user_ where p.creator_id = user_.id) as creator_local,
|
|
||||||
(select name from user_ where p.creator_id = user_.id) as creator_name,
|
|
||||||
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
|
|
||||||
(select actor_id from community where p.community_id = community.id) as community_actor_id,
|
|
||||||
(select local from community where p.community_id = community.id) as community_local,
|
|
||||||
(select name from community where p.community_id = community.id) as community_name,
|
|
||||||
(select removed from community c where p.community_id = c.id) as community_removed,
|
|
||||||
(select deleted from community c where p.community_id = c.id) as community_deleted,
|
|
||||||
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
|
|
||||||
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
|
|
||||||
coalesce(sum(pl.score), 0) as score,
|
|
||||||
count (case when pl.score = 1 then 1 else null end) as upvotes,
|
|
||||||
count (case when pl.score = -1 then 1 else null end) as downvotes,
|
|
||||||
hot_rank(coalesce(sum(pl.score) , 0),
|
|
||||||
(
|
|
||||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
|
||||||
else greatest(c.recent_comment_time, p.published)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
) as hot_rank,
|
|
||||||
(
|
|
||||||
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
|
|
||||||
else greatest(c.recent_comment_time, p.published)
|
|
||||||
end
|
|
||||||
) as newest_activity_time
|
|
||||||
from post p
|
|
||||||
left join post_like pl on p.id = pl.post_id
|
|
||||||
left join (
|
|
||||||
select post_id,
|
|
||||||
max(published) as recent_comment_time
|
|
||||||
from comment
|
|
||||||
group by 1
|
|
||||||
) c on p.id = c.post_id
|
|
||||||
group by p.id, c.recent_comment_time;
|
|
||||||
|
|
||||||
create materialized view post_aggregates_mview as select * from post_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view post_view as
|
|
||||||
with all_post as (
|
|
||||||
select
|
|
||||||
pa.*
|
|
||||||
from post_aggregates_view pa
|
|
||||||
)
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(pl.score, 0) as my_vote,
|
|
||||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
|
||||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
|
||||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_post ap
|
|
||||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as read,
|
|
||||||
null as saved
|
|
||||||
from all_post ap
|
|
||||||
;
|
|
||||||
|
|
||||||
create view post_mview as
|
|
||||||
with all_post as (
|
|
||||||
select
|
|
||||||
pa.*
|
|
||||||
from post_aggregates_mview pa
|
|
||||||
)
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(pl.score, 0) as my_vote,
|
|
||||||
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
|
|
||||||
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
|
|
||||||
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_post ap
|
|
||||||
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ap.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as read,
|
|
||||||
null as saved
|
|
||||||
from all_post ap
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
-- reply_view, comment_view, user_mention
|
|
||||||
drop view reply_view;
|
|
||||||
drop view user_mention_view;
|
|
||||||
drop view user_mention_mview;
|
|
||||||
drop view comment_view;
|
|
||||||
drop view comment_mview;
|
|
||||||
drop materialized view comment_aggregates_mview;
|
|
||||||
drop view comment_aggregates_view;
|
|
||||||
|
|
||||||
-- reply and comment view
|
|
||||||
create view comment_aggregates_view as
|
|
||||||
select
|
|
||||||
c.*,
|
|
||||||
(select community_id from post p where p.id = c.post_id),
|
|
||||||
(select co.actor_id from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_actor_id,
|
|
||||||
(select co.local from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_local,
|
|
||||||
(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
|
|
||||||
(select u.banned from user_ u where c.creator_id = u.id) as banned,
|
|
||||||
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
|
|
||||||
(select actor_id from user_ where c.creator_id = user_.id) as creator_actor_id,
|
|
||||||
(select local from user_ where c.creator_id = user_.id) as creator_local,
|
|
||||||
(select name from user_ where c.creator_id = user_.id) as creator_name,
|
|
||||||
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
|
|
||||||
coalesce(sum(cl.score), 0) as score,
|
|
||||||
count (case when cl.score = 1 then 1 else null end) as upvotes,
|
|
||||||
count (case when cl.score = -1 then 1 else null end) as downvotes,
|
|
||||||
hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
|
|
||||||
from comment c
|
|
||||||
left join comment_like cl on c.id = cl.comment_id
|
|
||||||
group by c.id;
|
|
||||||
|
|
||||||
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
|
|
||||||
|
|
||||||
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
|
|
||||||
|
|
||||||
create view comment_view as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_view ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as saved
|
|
||||||
from all_comment ac
|
|
||||||
;
|
|
||||||
|
|
||||||
create view comment_mview as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.*,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as subscribed,
|
|
||||||
null as saved
|
|
||||||
from all_comment ac
|
|
||||||
;
|
|
||||||
|
|
||||||
-- Do the reply_view referencing the comment_mview
|
|
||||||
create view reply_view as
|
|
||||||
with closereply as (
|
|
||||||
select
|
|
||||||
c2.id,
|
|
||||||
c2.creator_id as sender_id,
|
|
||||||
c.creator_id as recipient_id
|
|
||||||
from comment c
|
|
||||||
inner join comment c2 on c.id = c2.parent_id
|
|
||||||
where c2.creator_id != c.creator_id
|
|
||||||
-- Do union where post is null
|
|
||||||
union
|
|
||||||
select
|
|
||||||
c.id,
|
|
||||||
c.creator_id as sender_id,
|
|
||||||
p.creator_id as recipient_id
|
|
||||||
from comment c, post p
|
|
||||||
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
|
|
||||||
)
|
|
||||||
select cv.*,
|
|
||||||
closereply.recipient_id
|
|
||||||
from comment_mview cv, closereply
|
|
||||||
where closereply.id = cv.id
|
|
||||||
;
|
|
||||||
|
|
||||||
-- user mention
|
|
||||||
create view user_mention_view as
|
|
||||||
select
|
|
||||||
c.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
c.creator_id,
|
|
||||||
c.creator_actor_id,
|
|
||||||
c.creator_local,
|
|
||||||
c.post_id,
|
|
||||||
c.parent_id,
|
|
||||||
c.content,
|
|
||||||
c.removed,
|
|
||||||
um.read,
|
|
||||||
c.published,
|
|
||||||
c.updated,
|
|
||||||
c.deleted,
|
|
||||||
c.community_id,
|
|
||||||
c.community_actor_id,
|
|
||||||
c.community_local,
|
|
||||||
c.community_name,
|
|
||||||
c.banned,
|
|
||||||
c.banned_from_community,
|
|
||||||
c.creator_name,
|
|
||||||
c.creator_avatar,
|
|
||||||
c.score,
|
|
||||||
c.upvotes,
|
|
||||||
c.downvotes,
|
|
||||||
c.hot_rank,
|
|
||||||
c.user_id,
|
|
||||||
c.my_vote,
|
|
||||||
c.saved,
|
|
||||||
um.recipient_id,
|
|
||||||
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
|
|
||||||
(select local from user_ u where u.id = um.recipient_id) as recipient_local
|
|
||||||
from user_mention um, comment_view c
|
|
||||||
where um.comment_id = c.id;
|
|
||||||
|
|
||||||
|
|
||||||
create view user_mention_mview as
|
|
||||||
with all_comment as
|
|
||||||
(
|
|
||||||
select
|
|
||||||
ca.*
|
|
||||||
from comment_aggregates_mview ca
|
|
||||||
)
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
ac.creator_id,
|
|
||||||
ac.creator_actor_id,
|
|
||||||
ac.creator_local,
|
|
||||||
ac.post_id,
|
|
||||||
ac.parent_id,
|
|
||||||
ac.content,
|
|
||||||
ac.removed,
|
|
||||||
um.read,
|
|
||||||
ac.published,
|
|
||||||
ac.updated,
|
|
||||||
ac.deleted,
|
|
||||||
ac.community_id,
|
|
||||||
ac.community_actor_id,
|
|
||||||
ac.community_local,
|
|
||||||
ac.community_name,
|
|
||||||
ac.banned,
|
|
||||||
ac.banned_from_community,
|
|
||||||
ac.creator_name,
|
|
||||||
ac.creator_avatar,
|
|
||||||
ac.score,
|
|
||||||
ac.upvotes,
|
|
||||||
ac.downvotes,
|
|
||||||
ac.hot_rank,
|
|
||||||
u.id as user_id,
|
|
||||||
coalesce(cl.score, 0) as my_vote,
|
|
||||||
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
|
|
||||||
um.recipient_id,
|
|
||||||
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
|
|
||||||
(select local from user_ u where u.id = um.recipient_id) as recipient_local
|
|
||||||
from user_ u
|
|
||||||
cross join all_comment ac
|
|
||||||
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
|
|
||||||
left join user_mention um on um.comment_id = ac.id
|
|
||||||
|
|
||||||
union all
|
|
||||||
|
|
||||||
select
|
|
||||||
ac.id,
|
|
||||||
um.id as user_mention_id,
|
|
||||||
ac.creator_id,
|
|
||||||
ac.creator_actor_id,
|
|
||||||
ac.creator_local,
|
|
||||||
ac.post_id,
|
|
||||||
ac.parent_id,
|
|
||||||
ac.content,
|
|
||||||
ac.removed,
|
|
||||||
um.read,
|
|
||||||
ac.published,
|
|
||||||
ac.updated,
|
|
||||||
ac.deleted,
|
|
||||||
ac.community_id,
|
|
||||||
ac.community_actor_id,
|
|
||||||
ac.community_local,
|
|
||||||
ac.community_name,
|
|
||||||
ac.banned,
|
|
||||||
ac.banned_from_community,
|
|
||||||
ac.creator_name,
|
|
||||||
ac.creator_avatar,
|
|
||||||
ac.score,
|
|
||||||
ac.upvotes,
|
|
||||||
ac.downvotes,
|
|
||||||
ac.hot_rank,
|
|
||||||
null as user_id,
|
|
||||||
null as my_vote,
|
|
||||||
null as saved,
|
|
||||||
um.recipient_id,
|
|
||||||
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
|
|
||||||
(select local from user_ u where u.id = um.recipient_id) as recipient_local
|
|
||||||
from all_comment ac
|
|
||||||
left join user_mention um on um.comment_id = ac.id
|
|
||||||
;
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
-- The username index
|
|
||||||
drop index idx_user_name_lower_actor_id;
|
|
||||||
create unique index idx_user_name_lower on user_ (lower(name));
|
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
drop index idx_user_name_lower;
|
|
||||||
create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id));
|
|
|
@ -1,21 +0,0 @@
|
||||||
drop materialized view private_message_mview;
|
|
||||||
drop view private_message_view;
|
|
||||||
|
|
||||||
alter table private_message
|
|
||||||
drop column ap_id,
|
|
||||||
drop column local;
|
|
||||||
|
|
||||||
create view private_message_view as
|
|
||||||
select
|
|
||||||
pm.*,
|
|
||||||
u.name as creator_name,
|
|
||||||
u.avatar as creator_avatar,
|
|
||||||
u2.name as recipient_name,
|
|
||||||
u2.avatar as recipient_avatar
|
|
||||||
from private_message pm
|
|
||||||
inner join user_ u on u.id = pm.creator_id
|
|
||||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
|
||||||
|
|
||||||
create materialized view private_message_mview as select * from private_message_view;
|
|
||||||
|
|
||||||
create unique index idx_private_message_mview_id on private_message_mview (id);
|
|
|
@ -1,25 +0,0 @@
|
||||||
alter table private_message
|
|
||||||
add column ap_id character varying(255) not null default 'http://fake.com', -- This needs to be checked and updated in code, building from the site url if local
|
|
||||||
add column local boolean not null default true
|
|
||||||
;
|
|
||||||
|
|
||||||
drop materialized view private_message_mview;
|
|
||||||
drop view private_message_view;
|
|
||||||
create view private_message_view as
|
|
||||||
select
|
|
||||||
pm.*,
|
|
||||||
u.name as creator_name,
|
|
||||||
u.avatar as creator_avatar,
|
|
||||||
u.actor_id as creator_actor_id,
|
|
||||||
u.local as creator_local,
|
|
||||||
u2.name as recipient_name,
|
|
||||||
u2.avatar as recipient_avatar,
|
|
||||||
u2.actor_id as recipient_actor_id,
|
|
||||||
u2.local as recipient_local
|
|
||||||
from private_message pm
|
|
||||||
inner join user_ u on u.id = pm.creator_id
|
|
||||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
|
||||||
|
|
||||||
create materialized view private_message_mview as select * from private_message_view;
|
|
||||||
|
|
||||||
create unique index idx_private_message_mview_id on private_message_mview (id);
|
|
|
@ -11,12 +11,6 @@ declare -a arr=(
|
||||||
"https://torrents-csv.ml/service/search?q=wheel&page=1&type_=torrent"
|
"https://torrents-csv.ml/service/search?q=wheel&page=1&type_=torrent"
|
||||||
)
|
)
|
||||||
|
|
||||||
## check if ab installed
|
|
||||||
if ! [ -x "$(command -v ab)" ]; then
|
|
||||||
echo 'Error: ab (Apache Bench) is not installed. https://httpd.apache.org/docs/2.4/programs/ab.html' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
## now loop through the above array
|
## now loop through the above array
|
||||||
for i in "${arr[@]}"
|
for i in "${arr[@]}"
|
||||||
do
|
do
|
||||||
|
|
|
@ -15,12 +15,6 @@ declare -a arr=(
|
||||||
"/api/v1/post/list?sort=Hot&type_=All"
|
"/api/v1/post/list?sort=Hot&type_=All"
|
||||||
)
|
)
|
||||||
|
|
||||||
## check if ab installed
|
|
||||||
if ! [ -x "$(command -v ab)" ]; then
|
|
||||||
echo 'Error: ab (Apache Bench) is not installed. https://httpd.apache.org/docs/2.4/programs/ab.html' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
## now loop through the above array
|
## now loop through the above array
|
||||||
for path in "${arr[@]}"
|
for path in "${arr[@]}"
|
||||||
do
|
do
|
||||||
|
|
|
@ -1,39 +1,8 @@
|
||||||
use crate::{
|
use super::*;
|
||||||
api::{APIError, Oper, Perform},
|
use crate::send_email;
|
||||||
apub::{ApubLikeableType, ApubObjectType},
|
use crate::settings::Settings;
|
||||||
blocking,
|
use diesel::PgConnection;
|
||||||
db::{
|
|
||||||
comment::*,
|
|
||||||
comment_view::*,
|
|
||||||
community_view::*,
|
|
||||||
moderator::*,
|
|
||||||
post::*,
|
|
||||||
site_view::*,
|
|
||||||
user::*,
|
|
||||||
user_mention::*,
|
|
||||||
user_view::*,
|
|
||||||
Crud,
|
|
||||||
Likeable,
|
|
||||||
ListingType,
|
|
||||||
Saveable,
|
|
||||||
SortType,
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
remove_slurs,
|
|
||||||
scrape_text_for_mentions,
|
|
||||||
send_email,
|
|
||||||
settings::Settings,
|
|
||||||
websocket::{
|
|
||||||
server::{JoinCommunityRoom, SendComment},
|
|
||||||
UserOperation,
|
|
||||||
WebsocketInfo,
|
|
||||||
},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
MentionData,
|
|
||||||
};
|
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -95,15 +64,8 @@ pub struct GetCommentsResponse {
|
||||||
comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
impl Perform for Oper<CreateComment> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
type Response = CommentResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommentResponse, LemmyError> {
|
|
||||||
let data: &CreateComment = &self.data;
|
let data: &CreateComment = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -113,6 +75,19 @@ impl Perform for Oper<CreateComment> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
|
let hostname = &format!("https://{}", Settings::get().hostname);
|
||||||
|
|
||||||
|
// Check for a community ban
|
||||||
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
|
return Err(APIError::err("community_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a site ban
|
||||||
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
|
return Err(APIError::err("site_ban").into());
|
||||||
|
}
|
||||||
|
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
|
||||||
let comment_form = CommentForm {
|
let comment_form = CommentForm {
|
||||||
|
@ -123,54 +98,122 @@ impl Perform for Oper<CreateComment> {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
published: None,
|
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for a community ban
|
let inserted_comment = match Comment::create(&conn, &comment_form) {
|
||||||
let post_id = data.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for a site ban
|
|
||||||
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
|
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let comment_form2 = comment_form.clone();
|
|
||||||
let inserted_comment =
|
|
||||||
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
|
|
||||||
Ok(comment) => comment,
|
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inserted_comment_id = inserted_comment.id;
|
|
||||||
let updated_comment: Comment = match blocking(pool, move |conn| {
|
|
||||||
Comment::update_ap_id(&conn, inserted_comment_id)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
updated_comment
|
match Comment::update_ap_id(&conn, inserted_comment.id) {
|
||||||
.send_create(&user, &self.client, pool)
|
Ok(comment) => comment,
|
||||||
.await?;
|
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut recipient_ids = Vec::new();
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
let recipient_ids =
|
|
||||||
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
|
for username_mention in &extracted_usernames {
|
||||||
|
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||||
|
// You can't mention yourself
|
||||||
|
// 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
|
||||||
|
if mention_user.id != user_id {
|
||||||
|
recipient_ids.push(mention_user.id);
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user.id,
|
||||||
|
comment_id: inserted_comment.id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => error!("{}", &_e),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send an email to those users that have notifications on
|
||||||
|
if mention_user.send_notifications_to_email {
|
||||||
|
if let Some(mention_email) = mention_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Mentioned by {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &mention_email, &mention_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => error!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifs to the parent commenter / poster
|
||||||
|
match data.parent_id {
|
||||||
|
Some(parent_id) => {
|
||||||
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
|
if parent_comment.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||||
|
recipient_ids.push(parent_user.id);
|
||||||
|
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(comment_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => error!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Its a post
|
||||||
|
None => {
|
||||||
|
if post.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, post.creator_id)?;
|
||||||
|
recipient_ids.push(parent_user.id);
|
||||||
|
|
||||||
|
if parent_user.send_notifications_to_email {
|
||||||
|
if let Some(post_reply_email) = parent_user.email {
|
||||||
|
let subject = &format!(
|
||||||
|
"{} - Reply from {}",
|
||||||
|
Settings::get().hostname,
|
||||||
|
claims.username
|
||||||
|
);
|
||||||
|
let html = &format!(
|
||||||
|
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
||||||
|
claims.username, comment_form.content, hostname
|
||||||
|
);
|
||||||
|
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
||||||
|
Ok(_o) => _o,
|
||||||
|
Err(e) => error!("{}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// You like your own comment by default
|
// You like your own comment by default
|
||||||
let like_form = CommentLikeForm {
|
let like_form = CommentLikeForm {
|
||||||
|
@ -180,48 +223,22 @@ impl Perform for Oper<CreateComment> {
|
||||||
score: 1,
|
score: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
if blocking(pool, like).await?.is_err() {
|
Ok(like) => like,
|
||||||
return Err(APIError::err("couldnt_like_comment").into());
|
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
|
||||||
}
|
|
||||||
|
|
||||||
updated_comment.send_like(&user, &self.client, pool).await?;
|
|
||||||
|
|
||||||
let comment_view = blocking(pool, move |conn| {
|
|
||||||
CommentView::read(&conn, inserted_comment.id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let mut res = CommentResponse {
|
|
||||||
comment: comment_view,
|
|
||||||
recipient_ids,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
|
||||||
ws.chatserver.do_send(SendComment {
|
|
||||||
op: UserOperation::CreateComment,
|
|
||||||
comment: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// strip out the recipient_ids, so that
|
Ok(CommentResponse {
|
||||||
// users don't get double notifs
|
comment: comment_view,
|
||||||
res.recipient_ids = Vec::new();
|
recipient_ids,
|
||||||
}
|
})
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommentResponse> for Oper<EditComment> {
|
||||||
impl Perform for Oper<EditComment> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
type Response = CommentResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommentResponse, LemmyError> {
|
|
||||||
let data: &EditComment = &self.data;
|
let data: &EditComment = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -231,57 +248,38 @@ impl Perform for Oper<EditComment> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
|
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
|
||||||
let orig_comment =
|
|
||||||
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
|
|
||||||
|
|
||||||
// You are allowed to mark the comment as read even if you're banned.
|
// You are allowed to mark the comment as read even if you're banned.
|
||||||
if data.read.is_none() {
|
if data.read.is_none() {
|
||||||
// Verify its the creator or a mod, or an admin
|
// Verify its the creator or a mod, or an admin
|
||||||
let mut editors: Vec<i32> = vec![data.creator_id];
|
let mut editors: Vec<i32> = vec![data.creator_id];
|
||||||
let community_id = orig_comment.community_id;
|
|
||||||
editors.append(
|
editors.append(
|
||||||
&mut blocking(pool, move |conn| {
|
&mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)?
|
||||||
Ok(
|
.into_iter()
|
||||||
CommunityModeratorView::for_community(&conn, community_id)?
|
.map(|m| m.user_id)
|
||||||
.into_iter()
|
.collect(),
|
||||||
.map(|m| m.user_id)
|
|
||||||
.collect(),
|
|
||||||
) as Result<_, LemmyError>
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
editors.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
Ok(UserView::admins(conn)?.into_iter().map(|a| a.id).collect()) as Result<_, LemmyError>
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
);
|
||||||
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
|
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err("no_comment_edit_allowed").into());
|
return Err(APIError::err("no_comment_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let community_id = orig_comment.community_id;
|
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if user.banned {
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
let content_slurs_removed = remove_slurs(&data.content.to_owned());
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let read_comment = Comment::read(&conn, data.edit_id)?;
|
||||||
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
|
|
||||||
|
|
||||||
let comment_form = CommentForm {
|
let comment_form = CommentForm {
|
||||||
content: content_slurs_removed,
|
content: content_slurs_removed,
|
||||||
|
@ -291,7 +289,6 @@ impl Perform for Oper<EditComment> {
|
||||||
removed: data.removed.to_owned(),
|
removed: data.removed.to_owned(),
|
||||||
deleted: data.deleted.to_owned(),
|
deleted: data.deleted.to_owned(),
|
||||||
read: data.read.to_owned(),
|
read: data.read.to_owned(),
|
||||||
published: None,
|
|
||||||
updated: if data.read.is_some() {
|
updated: if data.read.is_some() {
|
||||||
orig_comment.updated
|
orig_comment.updated
|
||||||
} else {
|
} else {
|
||||||
|
@ -301,48 +298,58 @@ impl Perform for Oper<EditComment> {
|
||||||
local: read_comment.local,
|
local: read_comment.local,
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
|
||||||
let comment_form2 = comment_form.clone();
|
|
||||||
let updated_comment = match blocking(pool, move |conn| {
|
|
||||||
Comment::update(conn, edit_id, &comment_form2)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(comment) => comment,
|
Ok(comment) => comment,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
let mut recipient_ids = Vec::new();
|
||||||
if deleted {
|
|
||||||
updated_comment
|
// Scan the comment for user mentions, add those rows
|
||||||
.send_delete(&user, &self.client, pool)
|
let extracted_usernames = extract_usernames(&comment_form.content);
|
||||||
.await?;
|
|
||||||
} else {
|
for username_mention in &extracted_usernames {
|
||||||
updated_comment
|
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
if mention_user.is_ok() {
|
||||||
|
let mention_user_id = mention_user?.id;
|
||||||
|
|
||||||
|
// You can't mention yourself
|
||||||
|
// 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
|
||||||
|
if mention_user_id != user_id {
|
||||||
|
recipient_ids.push(mention_user_id);
|
||||||
|
|
||||||
|
let user_mention_form = UserMentionForm {
|
||||||
|
recipient_id: mention_user_id,
|
||||||
|
comment_id: data.edit_id,
|
||||||
|
read: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow this to fail softly, since comment edits might re-update or replace it
|
||||||
|
// Let the uniqueness handle this fail
|
||||||
|
match UserMention::create(&conn, &user_mention_form) {
|
||||||
|
Ok(_mention) => (),
|
||||||
|
Err(_e) => error!("{}", &_e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
|
||||||
if removed {
|
|
||||||
updated_comment
|
|
||||||
.send_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_comment
|
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_comment
|
|
||||||
.send_update(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_id = data.post_id;
|
// Add to recipient ids
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
match data.parent_id {
|
||||||
|
Some(parent_id) => {
|
||||||
let mentions = scrape_text_for_mentions(&comment_form.content);
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
|
if parent_comment.creator_id != user_id {
|
||||||
|
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||||
|
recipient_ids.push(parent_user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
|
recipient_ids.push(post.creator_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
if let Some(removed) = data.removed.to_owned() {
|
if let Some(removed) = data.removed.to_owned() {
|
||||||
|
@ -352,45 +359,20 @@ impl Perform for Oper<EditComment> {
|
||||||
removed: Some(removed),
|
removed: Some(removed),
|
||||||
reason: data.reason.to_owned(),
|
reason: data.reason.to_owned(),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
|
ModRemoveComment::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
let comment_view = blocking(pool, move |conn| {
|
|
||||||
CommentView::read(conn, edit_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let mut res = CommentResponse {
|
Ok(CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
};
|
})
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendComment {
|
|
||||||
op: UserOperation::EditComment,
|
|
||||||
comment: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// strip out the recipient_ids, so that
|
|
||||||
// users don't get double notifs
|
|
||||||
res.recipient_ids = Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommentResponse> for Oper<SaveComment> {
|
||||||
impl Perform for Oper<SaveComment> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
type Response = CommentResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommentResponse, LemmyError> {
|
|
||||||
let data: &SaveComment = &self.data;
|
let data: &SaveComment = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -406,22 +388,18 @@ impl Perform for Oper<SaveComment> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.save {
|
if data.save {
|
||||||
let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
|
match CommentSaved::save(&conn, &comment_saved_form) {
|
||||||
if blocking(pool, save_comment).await?.is_err() {
|
Ok(comment) => comment,
|
||||||
return Err(APIError::err("couldnt_save_comment").into());
|
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
|
match CommentSaved::unsave(&conn, &comment_saved_form) {
|
||||||
if blocking(pool, unsave_comment).await?.is_err() {
|
Ok(comment) => comment,
|
||||||
return Err(APIError::err("couldnt_save_comment").into());
|
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||||
let comment_view = blocking(pool, move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
comment: comment_view,
|
comment: comment_view,
|
||||||
|
@ -430,15 +408,8 @@ impl Perform for Oper<SaveComment> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommentResponse> for Oper<CreateCommentLike> {
|
||||||
impl Perform for Oper<CreateCommentLike> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
type Response = CommentResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommentResponse, LemmyError> {
|
|
||||||
let data: &CreateCommentLike = &self.data;
|
let data: &CreateCommentLike = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -452,40 +423,31 @@ impl Perform for Oper<CreateCommentLike> {
|
||||||
|
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
|
let site = SiteView::read(&conn)?;
|
||||||
if !site.enable_downvotes {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err("downvotes_disabled").into());
|
return Err(APIError::err("downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post_id = data.post_id;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
let community_id = post.community_id;
|
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
let comment = Comment::read(&conn, data.comment_id)?;
|
||||||
let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
|
|
||||||
|
|
||||||
// Add to recipient ids
|
// Add to recipient ids
|
||||||
match comment.parent_id {
|
match comment.parent_id {
|
||||||
Some(parent_id) => {
|
Some(parent_id) => {
|
||||||
let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
|
let parent_comment = Comment::read(&conn, parent_id)?;
|
||||||
if parent_comment.creator_id != user_id {
|
if parent_comment.creator_id != user_id {
|
||||||
let parent_user = blocking(pool, move |conn| {
|
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
|
||||||
User_::read(conn, parent_comment.creator_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
recipient_ids.push(parent_user.id);
|
recipient_ids.push(parent_user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -502,64 +464,29 @@ impl Perform for Oper<CreateCommentLike> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any likes first
|
// Remove any likes first
|
||||||
let like_form2 = like_form.clone();
|
CommentLike::remove(&conn, &like_form)?;
|
||||||
blocking(pool, move |conn| CommentLike::remove(conn, &like_form2)).await??;
|
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let like_form2 = like_form.clone();
|
let _inserted_like = match CommentLike::like(&conn, &like_form) {
|
||||||
let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
|
Ok(like) => like,
|
||||||
if blocking(pool, like).await?.is_err() {
|
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
|
||||||
return Err(APIError::err("couldnt_like_comment").into());
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if like_form.score == 1 {
|
|
||||||
comment.send_like(&user, &self.client, pool).await?;
|
|
||||||
} else if like_form.score == -1 {
|
|
||||||
comment.send_dislike(&user, &self.client, pool).await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
comment.send_undo_like(&user, &self.client, pool).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Have to refetch the comment to get the current state
|
// Have to refetch the comment to get the current state
|
||||||
let comment_id = data.comment_id;
|
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
|
||||||
let liked_comment = blocking(pool, move |conn| {
|
|
||||||
CommentView::read(conn, comment_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let mut res = CommentResponse {
|
Ok(CommentResponse {
|
||||||
comment: liked_comment,
|
comment: liked_comment,
|
||||||
recipient_ids,
|
recipient_ids,
|
||||||
};
|
})
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendComment {
|
|
||||||
op: UserOperation::CreateCommentLike,
|
|
||||||
comment: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// strip out the recipient_ids, so that
|
|
||||||
// users don't get double notifs
|
|
||||||
res.recipient_ids = Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetCommentsResponse> for Oper<GetComments> {
|
||||||
impl Perform for Oper<GetComments> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
|
||||||
type Response = GetCommentsResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetCommentsResponse, LemmyError> {
|
|
||||||
let data: &GetComments = &self.data;
|
let data: &GetComments = &self.data;
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
|
@ -578,157 +505,19 @@ impl Perform for Oper<GetComments> {
|
||||||
let type_ = ListingType::from_str(&data.type_)?;
|
let type_ = ListingType::from_str(&data.type_)?;
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let comments = match CommentQueryBuilder::create(&conn)
|
||||||
let page = data.page;
|
.listing_type(type_)
|
||||||
let limit = data.limit;
|
.sort(&sort)
|
||||||
let comments = blocking(pool, move |conn| {
|
.for_community_id(data.community_id)
|
||||||
CommentQueryBuilder::create(conn)
|
.my_user_id(user_id)
|
||||||
.listing_type(type_)
|
.page(data.page)
|
||||||
.sort(&sort)
|
.limit(data.limit)
|
||||||
.for_community_id(community_id)
|
.list()
|
||||||
.my_user_id(user_id)
|
{
|
||||||
.page(page)
|
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let comments = match comments {
|
|
||||||
Ok(comments) => comments,
|
Ok(comments) => comments,
|
||||||
Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
|
Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
// You don't need to join the specific community room, bc this is already handled by
|
|
||||||
// GetCommunity
|
|
||||||
if data.community_id.is_none() {
|
|
||||||
if let Some(id) = ws.id {
|
|
||||||
// 0 is the "all" community
|
|
||||||
ws.chatserver.do_send(JoinCommunityRoom {
|
|
||||||
community_id: 0,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GetCommentsResponse { comments })
|
Ok(GetCommentsResponse { comments })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_local_notifs(
|
|
||||||
mentions: Vec<MentionData>,
|
|
||||||
comment: Comment,
|
|
||||||
user: User_,
|
|
||||||
post: Post,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Vec<i32>, LemmyError> {
|
|
||||||
let ids = blocking(pool, move |conn| {
|
|
||||||
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_send_local_notifs(
|
|
||||||
conn: &diesel::PgConnection,
|
|
||||||
mentions: &[MentionData],
|
|
||||||
comment: &Comment,
|
|
||||||
user: &User_,
|
|
||||||
post: &Post,
|
|
||||||
) -> Vec<i32> {
|
|
||||||
let mut recipient_ids = Vec::new();
|
|
||||||
let hostname = &format!("https://{}", Settings::get().hostname);
|
|
||||||
|
|
||||||
// Send the local mentions
|
|
||||||
for mention in mentions
|
|
||||||
.iter()
|
|
||||||
.filter(|m| m.is_local() && m.name.ne(&user.name))
|
|
||||||
.collect::<Vec<&MentionData>>()
|
|
||||||
{
|
|
||||||
if let Ok(mention_user) = User_::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.id);
|
|
||||||
|
|
||||||
let user_mention_form = UserMentionForm {
|
|
||||||
recipient_id: mention_user.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
|
|
||||||
match UserMention::create(&conn, &user_mention_form) {
|
|
||||||
Ok(_mention) => (),
|
|
||||||
Err(_e) => error!("{}", &_e),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send an email to those users that have notifications on
|
|
||||||
if mention_user.send_notifications_to_email {
|
|
||||||
if let Some(mention_email) = mention_user.email {
|
|
||||||
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
|
|
||||||
let html = &format!(
|
|
||||||
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
|
||||||
user.name, comment.content, hostname
|
|
||||||
);
|
|
||||||
match send_email(subject, &mention_email, &mention_user.name, html) {
|
|
||||||
Ok(_o) => _o,
|
|
||||||
Err(e) => error!("{}", e),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notifs to the parent commenter / poster
|
|
||||||
match comment.parent_id {
|
|
||||||
Some(parent_id) => {
|
|
||||||
if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
|
|
||||||
if parent_comment.creator_id != user.id {
|
|
||||||
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
|
|
||||||
recipient_ids.push(parent_user.id);
|
|
||||||
|
|
||||||
if parent_user.send_notifications_to_email {
|
|
||||||
if let Some(comment_reply_email) = parent_user.email {
|
|
||||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
|
||||||
let html = &format!(
|
|
||||||
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
|
||||||
user.name, comment.content, hostname
|
|
||||||
);
|
|
||||||
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
|
|
||||||
Ok(_o) => _o,
|
|
||||||
Err(e) => error!("{}", e),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Its a post
|
|
||||||
None => {
|
|
||||||
if post.creator_id != user.id {
|
|
||||||
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
|
|
||||||
recipient_ids.push(parent_user.id);
|
|
||||||
|
|
||||||
if parent_user.send_notifications_to_email {
|
|
||||||
if let Some(post_reply_email) = parent_user.email {
|
|
||||||
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
|
|
||||||
let html = &format!(
|
|
||||||
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
|
|
||||||
user.name, comment.content, hostname
|
|
||||||
);
|
|
||||||
match send_email(subject, &post_reply_email, &parent_user.name, html) {
|
|
||||||
Ok(_o) => _o,
|
|
||||||
Err(e) => error!("{}", e),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
recipient_ids
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,28 +1,8 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::apub::puller::{fetch_all_communities, fetch_remote_community};
|
||||||
api::{APIError, Oper, Perform},
|
use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
|
||||||
apub::{
|
use crate::settings::Settings;
|
||||||
extensions::signatures::generate_actor_keypair,
|
use diesel::PgConnection;
|
||||||
make_apub_endpoint,
|
|
||||||
ActorType,
|
|
||||||
EndpointType,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
db::{Bannable, Crud, Followable, Joinable, SortType},
|
|
||||||
is_valid_community_name,
|
|
||||||
naive_from_unix,
|
|
||||||
naive_now,
|
|
||||||
slur_check,
|
|
||||||
slurs_vec_to_str,
|
|
||||||
websocket::{
|
|
||||||
server::{JoinCommunityRoom, SendCommunityRoomMessage},
|
|
||||||
UserOperation,
|
|
||||||
WebsocketInfo,
|
|
||||||
},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -61,6 +41,7 @@ pub struct ListCommunities {
|
||||||
pub page: Option<i64>,
|
pub page: Option<i64>,
|
||||||
pub limit: Option<i64>,
|
pub limit: Option<i64>,
|
||||||
pub auth: Option<String>,
|
pub auth: Option<String>,
|
||||||
|
pub local_only: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -78,7 +59,7 @@ pub struct BanFromCommunity {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct BanFromCommunityResponse {
|
pub struct BanFromCommunityResponse {
|
||||||
user: UserView,
|
user: UserView,
|
||||||
banned: bool,
|
banned: bool,
|
||||||
|
@ -92,7 +73,7 @@ pub struct AddModToCommunity {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AddModToCommunityResponse {
|
pub struct AddModToCommunityResponse {
|
||||||
moderators: Vec<CommunityModeratorView>,
|
moderators: Vec<CommunityModeratorView>,
|
||||||
}
|
}
|
||||||
|
@ -136,17 +117,17 @@ pub struct TransferCommunity {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
|
||||||
impl Perform for Oper<GetCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||||
type Response = GetCommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetCommunityResponse, LemmyError> {
|
|
||||||
let data: &GetCommunity = &self.data;
|
let data: &GetCommunity = &self.data;
|
||||||
|
|
||||||
|
if data.name.is_some()
|
||||||
|
&& Settings::get().federation.enabled
|
||||||
|
&& data.name.as_ref().unwrap().contains('@')
|
||||||
|
{
|
||||||
|
return fetch_remote_community(data.name.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
let user_id: Option<i32> = match &data.auth {
|
let user_id: Option<i32> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
|
@ -158,81 +139,47 @@ impl Perform for Oper<GetCommunity> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
|
let community_id = match data.id {
|
||||||
let community = match data.id {
|
Some(id) => id,
|
||||||
Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??,
|
None => {
|
||||||
None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? {
|
match Community::read_from_name(
|
||||||
Ok(community) => community,
|
&conn,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
|
||||||
},
|
) {
|
||||||
|
Ok(community) => community.id,
|
||||||
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let community_view = match CommunityView::read(&conn, community_id, user_id) {
|
||||||
let community_view = match blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, user_id)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
|
||||||
let moderators: Vec<CommunityModeratorView> = match blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
let site_creator_id = site.creator_id;
|
let mut admins = UserView::admins(&conn)?;
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||||
let creator_user = admins.remove(creator_index);
|
let creator_user = admins.remove(creator_index);
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
let online = if let Some(ws) = websocket_info {
|
// Return the jwt
|
||||||
if let Some(id) = ws.id {
|
Ok(GetCommunityResponse {
|
||||||
ws.chatserver.do_send(JoinCommunityRoom {
|
|
||||||
community_id: community.id,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
1
|
|
||||||
// let fut = async {
|
|
||||||
// ws.chatserver.send(GetCommunityUsersOnline {community_id}).await.unwrap()
|
|
||||||
// };
|
|
||||||
// Runtime::new().unwrap().block_on(fut)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = GetCommunityResponse {
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
admins,
|
||||||
online,
|
online: 0,
|
||||||
};
|
})
|
||||||
|
|
||||||
// Return the jwt
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommunityResponse> for Oper<CreateCommunity> {
|
||||||
impl Perform for Oper<CreateCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||||
type Response = CommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommunityResponse, LemmyError> {
|
|
||||||
let data: &CreateCommunity = &self.data;
|
let data: &CreateCommunity = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -254,20 +201,15 @@ impl Perform for Oper<CreateCommunity> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_valid_community_name(&data.name) {
|
|
||||||
return Err(APIError::err("invalid_community_name").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user_view.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// When you create a community, make sure the user becomes a moderator and a follower
|
// When you create a community, make sure the user becomes a moderator and a follower
|
||||||
let keypair = generate_actor_keypair()?;
|
let (community_public_key, community_private_key) = gen_keypair_str();
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -281,42 +223,39 @@ impl Perform for Oper<CreateCommunity> {
|
||||||
updated: None,
|
updated: None,
|
||||||
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
|
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
|
||||||
local: true,
|
local: true,
|
||||||
private_key: Some(keypair.private_key),
|
private_key: Some(community_private_key),
|
||||||
public_key: Some(keypair.public_key),
|
public_key: Some(community_public_key),
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community =
|
let inserted_community = match Community::create(&conn, &community_form) {
|
||||||
match blocking(pool, move |conn| Community::create(conn, &community_form)).await? {
|
Ok(community) => community,
|
||||||
Ok(community) => community,
|
Err(_e) => return Err(APIError::err("community_already_exists").into()),
|
||||||
Err(_e) => return Err(APIError::err("community_already_exists").into()),
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let community_moderator_form = CommunityModeratorForm {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
user_id,
|
user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
let _inserted_community_moderator =
|
||||||
if blocking(pool, join).await?.is_err() {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
return Err(APIError::err("community_moderator_already_exists").into());
|
Ok(user) => user,
|
||||||
}
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
|
};
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
user_id,
|
user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
|
let _inserted_community_follower =
|
||||||
if blocking(pool, follow).await?.is_err() {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
return Err(APIError::err("community_follower_already_exists").into());
|
Ok(user) => user,
|
||||||
}
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
|
};
|
||||||
|
|
||||||
let community_view = blocking(pool, move |conn| {
|
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
|
||||||
CommunityView::read(conn, inserted_community.id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(CommunityResponse {
|
Ok(CommunityResponse {
|
||||||
community: community_view,
|
community: community_view,
|
||||||
|
@ -324,15 +263,8 @@ impl Perform for Oper<CreateCommunity> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommunityResponse> for Oper<EditCommunity> {
|
||||||
impl Perform for Oper<EditCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||||
type Response = CommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommunityResponse, LemmyError> {
|
|
||||||
let data: &EditCommunity = &self.data;
|
let data: &EditCommunity = &self.data;
|
||||||
|
|
||||||
if let Err(slurs) = slur_check(&data.name) {
|
if let Err(slurs) = slur_check(&data.name) {
|
||||||
|
@ -354,40 +286,27 @@ impl Perform for Oper<EditCommunity> {
|
||||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_valid_community_name(&data.name) {
|
|
||||||
return Err(APIError::err("invalid_community_name").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify its a mod
|
// Verify its a mod
|
||||||
let edit_id = data.edit_id;
|
|
||||||
let mut editors: Vec<i32> = Vec::new();
|
let mut editors: Vec<i32> = Vec::new();
|
||||||
editors.append(
|
editors.append(
|
||||||
&mut blocking(pool, move |conn| {
|
&mut CommunityModeratorView::for_community(&conn, data.edit_id)?
|
||||||
CommunityModeratorView::for_community(conn, edit_id)
|
.into_iter()
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
.map(|m| m.user_id)
|
||||||
})
|
.collect(),
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
editors.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
);
|
||||||
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err("no_community_edit_allowed").into());
|
return Err(APIError::err("no_community_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let read_community = Community::read(&conn, data.edit_id)?;
|
||||||
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
|
||||||
|
|
||||||
let community_form = CommunityForm {
|
let community_form = CommunityForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -404,15 +323,9 @@ impl Perform for Oper<EditCommunity> {
|
||||||
private_key: read_community.private_key,
|
private_key: read_community.private_key,
|
||||||
public_key: read_community.public_key,
|
public_key: read_community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
|
||||||
let updated_community = match blocking(pool, move |conn| {
|
|
||||||
Community::update(conn, edit_id, &community_form)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
};
|
};
|
||||||
|
@ -430,70 +343,28 @@ impl Perform for Oper<EditCommunity> {
|
||||||
reason: data.reason.to_owned(),
|
reason: data.reason.to_owned(),
|
||||||
expires,
|
expires,
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
|
ModRemoveCommunity::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
if deleted {
|
|
||||||
updated_community
|
|
||||||
.send_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_community
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
|
||||||
if removed {
|
|
||||||
updated_community
|
|
||||||
.send_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
updated_community
|
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
Ok(CommunityResponse {
|
||||||
let community_view = blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, edit_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = CommunityResponse {
|
|
||||||
community: community_view,
|
community: community_view,
|
||||||
};
|
})
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
// Strip out the user id and subscribed when sending to others
|
|
||||||
let mut res_sent = res.clone();
|
|
||||||
res_sent.community.user_id = None;
|
|
||||||
res_sent.community.subscribed = None;
|
|
||||||
|
|
||||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperation::EditCommunity,
|
|
||||||
response: res_sent,
|
|
||||||
community_id: data.edit_id,
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
|
||||||
impl Perform for Oper<ListCommunities> {
|
fn perform(&self, conn: &PgConnection) -> Result<ListCommunitiesResponse, Error> {
|
||||||
type Response = ListCommunitiesResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<ListCommunitiesResponse, LemmyError> {
|
|
||||||
let data: &ListCommunities = &self.data;
|
let data: &ListCommunities = &self.data;
|
||||||
|
|
||||||
|
let local_only = data.local_only.unwrap_or(false);
|
||||||
|
if Settings::get().federation.enabled && !local_only {
|
||||||
|
return Ok(ListCommunitiesResponse {
|
||||||
|
communities: fetch_all_communities()?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => Some(claims.claims),
|
||||||
|
@ -514,33 +385,21 @@ impl Perform for Oper<ListCommunities> {
|
||||||
|
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let page = data.page;
|
let communities = CommunityQueryBuilder::create(&conn)
|
||||||
let limit = data.limit;
|
.sort(&sort)
|
||||||
let communities = blocking(pool, move |conn| {
|
.for_user(user_id)
|
||||||
CommunityQueryBuilder::create(conn)
|
.show_nsfw(show_nsfw)
|
||||||
.sort(&sort)
|
.page(data.page)
|
||||||
.for_user(user_id)
|
.limit(data.limit)
|
||||||
.show_nsfw(show_nsfw)
|
.list()?;
|
||||||
.page(page)
|
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(ListCommunitiesResponse { communities })
|
Ok(ListCommunitiesResponse { communities })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<CommunityResponse> for Oper<FollowCommunity> {
|
||||||
impl Perform for Oper<FollowCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommunityResponse, Error> {
|
||||||
type Response = CommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<CommunityResponse, LemmyError> {
|
|
||||||
let data: &FollowCommunity = &self.data;
|
let data: &FollowCommunity = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -550,53 +409,24 @@ impl Perform for Oper<FollowCommunity> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
community_id: data.community_id,
|
community_id: data.community_id,
|
||||||
user_id,
|
user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
if community.local {
|
if data.follow {
|
||||||
if data.follow {
|
match CommunityFollower::follow(&conn, &community_follower_form) {
|
||||||
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
|
Ok(user) => user,
|
||||||
if blocking(pool, follow).await?.is_err() {
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
return Err(APIError::err("community_follower_already_exists").into());
|
};
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let unfollow =
|
|
||||||
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
|
|
||||||
if blocking(pool, unfollow).await?.is_err() {
|
|
||||||
return Err(APIError::err("community_follower_already_exists").into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
match CommunityFollower::ignore(&conn, &community_follower_form) {
|
||||||
|
Ok(user) => user,
|
||||||
if data.follow {
|
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
|
||||||
// Dont actually add to the community followers here, because you need
|
};
|
||||||
// to wait for the accept
|
|
||||||
user
|
|
||||||
.send_follow(&community.actor_id, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
user
|
|
||||||
.send_unfollow(&community.actor_id, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
let unfollow =
|
|
||||||
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
|
|
||||||
if blocking(pool, unfollow).await?.is_err() {
|
|
||||||
return Err(APIError::err("community_follower_already_exists").into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: this needs to return a "pending" state, until Accept is received from the remote server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
|
||||||
let community_view = blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(CommunityResponse {
|
Ok(CommunityResponse {
|
||||||
community: community_view,
|
community: community_view,
|
||||||
|
@ -604,15 +434,8 @@ impl Perform for Oper<FollowCommunity> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
|
||||||
impl Perform for Oper<GetFollowedCommunities> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||||
type Response = GetFollowedCommunitiesResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
|
|
||||||
let data: &GetFollowedCommunities = &self.data;
|
let data: &GetFollowedCommunities = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -622,29 +445,19 @@ impl Perform for Oper<GetFollowedCommunities> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let communities = match blocking(pool, move |conn| {
|
let communities: Vec<CommunityFollowerView> =
|
||||||
CommunityFollowerView::for_user(conn, user_id)
|
match CommunityFollowerView::for_user(&conn, user_id) {
|
||||||
})
|
Ok(communities) => communities,
|
||||||
.await?
|
Err(_e) => return Err(APIError::err("system_err_login").into()),
|
||||||
{
|
};
|
||||||
Ok(communities) => communities,
|
|
||||||
_ => return Err(APIError::err("system_err_login").into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetFollowedCommunitiesResponse { communities })
|
Ok(GetFollowedCommunitiesResponse { communities })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
|
||||||
impl Perform for Oper<BanFromCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<BanFromCommunityResponse, Error> {
|
||||||
type Response = BanFromCommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<BanFromCommunityResponse, LemmyError> {
|
|
||||||
let data: &BanFromCommunity = &self.data;
|
let data: &BanFromCommunity = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -660,15 +473,15 @@ impl Perform for Oper<BanFromCommunity> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.ban {
|
if data.ban {
|
||||||
let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
|
match CommunityUserBan::ban(&conn, &community_user_ban_form) {
|
||||||
if blocking(pool, ban).await?.is_err() {
|
Ok(user) => user,
|
||||||
return Err(APIError::err("community_user_already_banned").into());
|
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
|
match CommunityUserBan::unban(&conn, &community_user_ban_form) {
|
||||||
if blocking(pool, unban).await?.is_err() {
|
Ok(user) => user,
|
||||||
return Err(APIError::err("community_user_already_banned").into());
|
Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -685,38 +498,19 @@ impl Perform for Oper<BanFromCommunity> {
|
||||||
banned: Some(data.ban),
|
banned: Some(data.ban),
|
||||||
expires,
|
expires,
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModBanFromCommunity::create(conn, &form)).await??;
|
ModBanFromCommunity::create(&conn, &form)?;
|
||||||
|
|
||||||
let user_id = data.user_id;
|
let user_view = UserView::read(&conn, data.user_id)?;
|
||||||
let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
|
||||||
|
|
||||||
let res = BanFromCommunityResponse {
|
Ok(BanFromCommunityResponse {
|
||||||
user: user_view,
|
user: user_view,
|
||||||
banned: data.ban,
|
banned: data.ban,
|
||||||
};
|
})
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperation::BanFromCommunity,
|
|
||||||
response: res.clone(),
|
|
||||||
community_id: data.community_id,
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
|
||||||
impl Perform for Oper<AddModToCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<AddModToCommunityResponse, Error> {
|
||||||
type Response = AddModToCommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<AddModToCommunityResponse, LemmyError> {
|
|
||||||
let data: &AddModToCommunity = &self.data;
|
let data: &AddModToCommunity = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -732,15 +526,15 @@ impl Perform for Oper<AddModToCommunity> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.added {
|
if data.added {
|
||||||
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
if blocking(pool, join).await?.is_err() {
|
Ok(user) => user,
|
||||||
return Err(APIError::err("community_moderator_already_exists").into());
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
|
match CommunityModerator::leave(&conn, &community_moderator_form) {
|
||||||
if blocking(pool, leave).await?.is_err() {
|
Ok(user) => user,
|
||||||
return Err(APIError::err("community_moderator_already_exists").into());
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -750,38 +544,16 @@ impl Perform for Oper<AddModToCommunity> {
|
||||||
community_id: data.community_id,
|
community_id: data.community_id,
|
||||||
removed: Some(!data.added),
|
removed: Some(!data.added),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
|
ModAddCommunity::create(&conn, &form)?;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
||||||
let moderators = blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = AddModToCommunityResponse { moderators };
|
Ok(AddModToCommunityResponse { moderators })
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendCommunityRoomMessage {
|
|
||||||
op: UserOperation::AddModToCommunity,
|
|
||||||
response: res.clone(),
|
|
||||||
community_id: data.community_id,
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
|
||||||
impl Perform for Oper<TransferCommunity> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetCommunityResponse, Error> {
|
||||||
type Response = GetCommunityResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetCommunityResponse, LemmyError> {
|
|
||||||
let data: &TransferCommunity = &self.data;
|
let data: &TransferCommunity = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -791,14 +563,10 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let read_community = Community::read(&conn, data.community_id)?;
|
||||||
let read_community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let site_creator_id =
|
|
||||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
|
|
||||||
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
|
let mut admins = UserView::admins(&conn)?;
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||||
let creator_user = admins.remove(creator_index);
|
let creator_user = admins.remove(creator_index);
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
@ -813,7 +581,7 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
title: read_community.title,
|
title: read_community.title,
|
||||||
description: read_community.description,
|
description: read_community.description,
|
||||||
category_id: read_community.category_id,
|
category_id: read_community.category_id,
|
||||||
creator_id: data.user_id, // This makes the new user the community creator
|
creator_id: data.user_id,
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
nsfw: read_community.nsfw,
|
nsfw: read_community.nsfw,
|
||||||
|
@ -823,21 +591,15 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
private_key: read_community.private_key,
|
private_key: read_community.private_key,
|
||||||
public_key: read_community.public_key,
|
public_key: read_community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
|
||||||
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
|
Ok(community) => community,
|
||||||
if blocking(pool, update).await?.is_err() {
|
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
|
||||||
return Err(APIError::err("couldnt_update_community").into());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// You also have to re-do the community_moderator table, reordering it.
|
// You also have to re-do the community_moderator table, reordering it.
|
||||||
let community_id = data.community_id;
|
let mut community_mods = CommunityModeratorView::for_community(&conn, data.community_id)?;
|
||||||
let mut community_mods = blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let creator_index = community_mods
|
let creator_index = community_mods
|
||||||
.iter()
|
.iter()
|
||||||
.position(|r| r.user_id == data.user_id)
|
.position(|r| r.user_id == data.user_id)
|
||||||
|
@ -845,23 +607,19 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
let creator_user = community_mods.remove(creator_index);
|
let creator_user = community_mods.remove(creator_index);
|
||||||
community_mods.insert(0, creator_user);
|
community_mods.insert(0, creator_user);
|
||||||
|
|
||||||
let community_id = data.community_id;
|
CommunityModerator::delete_for_community(&conn, data.community_id)?;
|
||||||
blocking(pool, move |conn| {
|
|
||||||
CommunityModerator::delete_for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO: this should probably be a bulk operation
|
|
||||||
for cmod in &community_mods {
|
for cmod in &community_mods {
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let community_moderator_form = CommunityModeratorForm {
|
||||||
community_id: cmod.community_id,
|
community_id: cmod.community_id,
|
||||||
user_id: cmod.user_id,
|
user_id: cmod.user_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
|
let _inserted_community_moderator =
|
||||||
if blocking(pool, join).await?.is_err() {
|
match CommunityModerator::join(&conn, &community_moderator_form) {
|
||||||
return Err(APIError::err("community_moderator_already_exists").into());
|
Ok(user) => user,
|
||||||
}
|
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -871,24 +629,14 @@ impl Perform for Oper<TransferCommunity> {
|
||||||
community_id: data.community_id,
|
community_id: data.community_id,
|
||||||
removed: Some(false),
|
removed: Some(false),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
|
ModAddCommunity::create(&conn, &form)?;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
|
||||||
let community_view = match blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(community) => community,
|
Ok(community) => community,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
|
||||||
let moderators = match blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(moderators) => moderators,
|
Ok(moderators) => moderators,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,29 @@
|
||||||
|
use crate::db::category::*;
|
||||||
|
use crate::db::comment::*;
|
||||||
|
use crate::db::comment_view::*;
|
||||||
|
use crate::db::community::*;
|
||||||
|
use crate::db::community_view::*;
|
||||||
|
use crate::db::moderator::*;
|
||||||
|
use crate::db::moderator_views::*;
|
||||||
|
use crate::db::password_reset_request::*;
|
||||||
|
use crate::db::post::*;
|
||||||
|
use crate::db::post_view::*;
|
||||||
|
use crate::db::private_message::*;
|
||||||
|
use crate::db::private_message_view::*;
|
||||||
|
use crate::db::site::*;
|
||||||
|
use crate::db::site_view::*;
|
||||||
|
use crate::db::user::*;
|
||||||
|
use crate::db::user_mention::*;
|
||||||
|
use crate::db::user_mention_view::*;
|
||||||
|
use crate::db::user_view::*;
|
||||||
|
use crate::db::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
|
extract_usernames, fetch_iframely_and_pictshare_data, naive_from_unix, naive_now, remove_slurs,
|
||||||
websocket::WebsocketInfo,
|
slur_check, slurs_vec_to_str,
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
};
|
||||||
use actix_web::client::Client;
|
use diesel::PgConnection;
|
||||||
|
use failure::Error;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
|
@ -28,22 +47,16 @@ impl APIError {
|
||||||
|
|
||||||
pub struct Oper<T> {
|
pub struct Oper<T> {
|
||||||
data: T,
|
data: T,
|
||||||
client: Client,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Data> Oper<Data> {
|
impl<T> Oper<T> {
|
||||||
pub fn new(data: Data, client: Client) -> Oper<Data> {
|
pub fn new(data: T) -> Oper<T> {
|
||||||
Oper { data, client }
|
Oper { data }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
pub trait Perform<T> {
|
||||||
pub trait Perform {
|
fn perform(&self, conn: &PgConnection) -> Result<T, Error>
|
||||||
type Response: serde::ser::Serialize + Send;
|
where
|
||||||
|
T: Sized;
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<Self::Response, LemmyError>;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,9 @@
|
||||||
use crate::{
|
use super::*;
|
||||||
api::{APIError, Oper, Perform},
|
use crate::settings::Settings;
|
||||||
apub::{ApubLikeableType, ApubObjectType},
|
use diesel::PgConnection;
|
||||||
blocking,
|
|
||||||
db::{
|
|
||||||
comment_view::*,
|
|
||||||
community_view::*,
|
|
||||||
moderator::*,
|
|
||||||
post::*,
|
|
||||||
post_view::*,
|
|
||||||
site::*,
|
|
||||||
site_view::*,
|
|
||||||
user::*,
|
|
||||||
user_view::*,
|
|
||||||
Crud,
|
|
||||||
Likeable,
|
|
||||||
ListingType,
|
|
||||||
Saveable,
|
|
||||||
SortType,
|
|
||||||
},
|
|
||||||
fetch_iframely_and_pictrs_data,
|
|
||||||
naive_now,
|
|
||||||
slur_check,
|
|
||||||
slurs_vec_to_str,
|
|
||||||
websocket::{
|
|
||||||
server::{JoinCommunityRoom, JoinPostRoom, SendPost},
|
|
||||||
UserOperation,
|
|
||||||
WebsocketInfo,
|
|
||||||
},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreatePost {
|
pub struct CreatePost {
|
||||||
name: String,
|
name: String,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
|
@ -110,15 +80,8 @@ pub struct SavePost {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<PostResponse> for Oper<CreatePost> {
|
||||||
impl Perform for Oper<CreatePost> {
|
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||||
type Response = PostResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<PostResponse, LemmyError> {
|
|
||||||
let data: &CreatePost = &self.data;
|
let data: &CreatePost = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -139,22 +102,18 @@ impl Perform for Oper<CreatePost> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let community_id = data.community_id;
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Iframely and pictrs cached image
|
// Fetch Iframely and Pictshare cached image
|
||||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
|
||||||
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
fetch_iframely_and_pictshare_data(data.url.to_owned());
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -171,13 +130,12 @@ impl Perform for Oper<CreatePost> {
|
||||||
embed_title: iframely_title,
|
embed_title: iframely_title,
|
||||||
embed_description: iframely_description,
|
embed_description: iframely_description,
|
||||||
embed_html: iframely_html,
|
embed_html: iframely_html,
|
||||||
thumbnail_url: pictrs_thumbnail,
|
thumbnail_url: pictshare_thumbnail,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_post = match blocking(pool, move |conn| Post::create(conn, &post_form)).await? {
|
let inserted_post = match Post::create(&conn, &post_form) {
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_type = if e.to_string() == "value too long for type character varying(200)" {
|
let err_type = if e.to_string() == "value too long for type character varying(200)" {
|
||||||
|
@ -190,14 +148,10 @@ impl Perform for Oper<CreatePost> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_post_id = inserted_post.id;
|
match Post::update_ap_id(&conn, inserted_post.id) {
|
||||||
let updated_post =
|
Ok(post) => post,
|
||||||
match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? {
|
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||||
Ok(post) => post,
|
};
|
||||||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
updated_post.send_create(&user, &self.client, pool).await?;
|
|
||||||
|
|
||||||
// They like their own post by default
|
// They like their own post by default
|
||||||
let like_form = PostLikeForm {
|
let like_form = PostLikeForm {
|
||||||
|
@ -206,47 +160,24 @@ impl Perform for Oper<CreatePost> {
|
||||||
score: 1,
|
score: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
|
// Only add the like if the score isnt 0
|
||||||
if blocking(pool, like).await?.is_err() {
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
return Err(APIError::err("couldnt_like_post").into());
|
Ok(like) => like,
|
||||||
}
|
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||||
|
};
|
||||||
updated_post.send_like(&user, &self.client, pool).await?;
|
|
||||||
|
|
||||||
// Refetch the view
|
// Refetch the view
|
||||||
let inserted_post_id = inserted_post.id;
|
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
|
||||||
let post_view = match blocking(pool, move |conn| {
|
|
||||||
PostView::read(conn, inserted_post_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = PostResponse { post: post_view };
|
Ok(PostResponse { post: post_view })
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePost,
|
|
||||||
post: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetPostResponse> for Oper<GetPost> {
|
||||||
impl Perform for Oper<GetPost> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetPostResponse, Error> {
|
||||||
type Response = GetPostResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetPostResponse, LemmyError> {
|
|
||||||
let data: &GetPost = &self.data;
|
let data: &GetPost = &self.data;
|
||||||
|
|
||||||
let user_id: Option<i32> = match &data.auth {
|
let user_id: Option<i32> = match &data.auth {
|
||||||
|
@ -260,60 +191,27 @@ impl Perform for Oper<GetPost> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = data.id;
|
let post_view = match PostView::read(&conn, data.id, user_id) {
|
||||||
let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? {
|
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = data.id;
|
let comments = CommentQueryBuilder::create(&conn)
|
||||||
let comments = blocking(pool, move |conn| {
|
.for_post_id(data.id)
|
||||||
CommentQueryBuilder::create(conn)
|
.my_user_id(user_id)
|
||||||
.for_post_id(id)
|
.limit(9999)
|
||||||
.my_user_id(user_id)
|
.list()?;
|
||||||
.limit(9999)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = post_view.community_id;
|
let community = CommunityView::read(&conn, post_view.community_id, user_id)?;
|
||||||
let community = blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, user_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = post_view.community_id;
|
let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?;
|
||||||
let moderators = blocking(pool, move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let site_creator_id =
|
let site_creator_id = Site::read(&conn, 1)?.creator_id;
|
||||||
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
|
let mut admins = UserView::admins(&conn)?;
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||||
let creator_user = admins.remove(creator_index);
|
let creator_user = admins.remove(creator_index);
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
let online = if let Some(ws) = websocket_info {
|
|
||||||
if let Some(id) = ws.id {
|
|
||||||
ws.chatserver.do_send(JoinPostRoom {
|
|
||||||
post_id: data.id,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
1
|
|
||||||
// let fut = async {
|
|
||||||
// ws.chatserver.send(GetPostUsersOnline {post_id: data.id}).await.unwrap()
|
|
||||||
// };
|
|
||||||
// Runtime::new().unwrap().block_on(fut)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(GetPostResponse {
|
Ok(GetPostResponse {
|
||||||
post: post_view,
|
post: post_view,
|
||||||
|
@ -321,22 +219,20 @@ impl Perform for Oper<GetPost> {
|
||||||
community,
|
community,
|
||||||
moderators,
|
moderators,
|
||||||
admins,
|
admins,
|
||||||
online,
|
online: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetPostsResponse> for Oper<GetPosts> {
|
||||||
impl Perform for Oper<GetPosts> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetPostsResponse, Error> {
|
||||||
type Response = GetPostsResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetPostsResponse, LemmyError> {
|
|
||||||
let data: &GetPosts = &self.data;
|
let data: &GetPosts = &self.data;
|
||||||
|
|
||||||
|
if Settings::get().federation.enabled {
|
||||||
|
// TODO: intercept here (but the type is wrong)
|
||||||
|
//get_remote_community_posts(get_posts.community_id.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
let user_claims: Option<Claims> = match &data.auth {
|
let user_claims: Option<Claims> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => Some(claims.claims),
|
Ok(claims) => Some(claims.claims),
|
||||||
|
@ -358,53 +254,26 @@ impl Perform for Oper<GetPosts> {
|
||||||
let type_ = ListingType::from_str(&data.type_)?;
|
let type_ = ListingType::from_str(&data.type_)?;
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
|
|
||||||
let page = data.page;
|
let posts = match PostQueryBuilder::create(&conn)
|
||||||
let limit = data.limit;
|
.listing_type(type_)
|
||||||
let community_id = data.community_id;
|
.sort(&sort)
|
||||||
let posts = match blocking(pool, move |conn| {
|
.show_nsfw(show_nsfw)
|
||||||
PostQueryBuilder::create(conn)
|
.for_community_id(data.community_id)
|
||||||
.listing_type(type_)
|
.my_user_id(user_id)
|
||||||
.sort(&sort)
|
.page(data.page)
|
||||||
.show_nsfw(show_nsfw)
|
.limit(data.limit)
|
||||||
.for_community_id(community_id)
|
.list()
|
||||||
.my_user_id(user_id)
|
|
||||||
.page(page)
|
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
{
|
||||||
Ok(posts) => posts,
|
Ok(posts) => posts,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
|
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
// You don't need to join the specific community room, bc this is already handled by
|
|
||||||
// GetCommunity
|
|
||||||
if data.community_id.is_none() {
|
|
||||||
if let Some(id) = ws.id {
|
|
||||||
// 0 is the "all" community
|
|
||||||
ws.chatserver.do_send(JoinCommunityRoom {
|
|
||||||
community_id: 0,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(GetPostsResponse { posts })
|
Ok(GetPostsResponse { posts })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<PostResponse> for Oper<CreatePostLike> {
|
||||||
impl Perform for Oper<CreatePostLike> {
|
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||||
type Response = PostResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<PostResponse, LemmyError> {
|
|
||||||
let data: &CreatePostLike = &self.data;
|
let data: &CreatePostLike = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -416,26 +285,20 @@ impl Perform for Oper<CreatePostLike> {
|
||||||
|
|
||||||
// Don't do a downvote if site has downvotes disabled
|
// Don't do a downvote if site has downvotes disabled
|
||||||
if data.score == -1 {
|
if data.score == -1 {
|
||||||
let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
|
let site = SiteView::read(&conn)?;
|
||||||
if !site.enable_downvotes {
|
if !site.enable_downvotes {
|
||||||
return Err(APIError::err("downvotes_disabled").into());
|
return Err(APIError::err("downvotes_disabled").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let post_id = data.post_id;
|
let post = Post::read(&conn, data.post_id)?;
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,60 +309,29 @@ impl Perform for Oper<CreatePostLike> {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any likes first
|
// Remove any likes first
|
||||||
let like_form2 = like_form.clone();
|
PostLike::remove(&conn, &like_form)?;
|
||||||
blocking(pool, move |conn| PostLike::remove(conn, &like_form2)).await??;
|
|
||||||
|
|
||||||
// Only add the like if the score isnt 0
|
// Only add the like if the score isnt 0
|
||||||
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
|
||||||
if do_add {
|
if do_add {
|
||||||
let like_form2 = like_form.clone();
|
let _inserted_like = match PostLike::like(&conn, &like_form) {
|
||||||
let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
|
Ok(like) => like,
|
||||||
if blocking(pool, like).await?.is_err() {
|
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
|
||||||
return Err(APIError::err("couldnt_like_post").into());
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if like_form.score == 1 {
|
|
||||||
post.send_like(&user, &self.client, pool).await?;
|
|
||||||
} else if like_form.score == -1 {
|
|
||||||
post.send_dislike(&user, &self.client, pool).await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
post.send_undo_like(&user, &self.client, pool).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_id = data.post_id;
|
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
|
||||||
let post_view = match blocking(pool, move |conn| {
|
|
||||||
PostView::read(conn, post_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let res = PostResponse { post: post_view };
|
// just output the score
|
||||||
|
Ok(PostResponse { post: post_view })
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendPost {
|
|
||||||
op: UserOperation::CreatePostLike,
|
|
||||||
post: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<PostResponse> for Oper<EditPost> {
|
||||||
impl Perform for Oper<EditPost> {
|
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||||
type Response = PostResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<PostResponse, LemmyError> {
|
|
||||||
let data: &EditPost = &self.data;
|
let data: &EditPost = &self.data;
|
||||||
|
|
||||||
if let Err(slurs) = slur_check(&data.name) {
|
if let Err(slurs) = slur_check(&data.name) {
|
||||||
|
@ -520,45 +352,33 @@ impl Perform for Oper<EditPost> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Verify its the creator or a mod or admin
|
// Verify its the creator or a mod or admin
|
||||||
let community_id = data.community_id;
|
|
||||||
let mut editors: Vec<i32> = vec![data.creator_id];
|
let mut editors: Vec<i32> = vec![data.creator_id];
|
||||||
editors.append(
|
editors.append(
|
||||||
&mut blocking(pool, move |conn| {
|
&mut CommunityModeratorView::for_community(&conn, data.community_id)?
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
.into_iter()
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
.map(|m| m.user_id)
|
||||||
})
|
.collect(),
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
editors.append(
|
|
||||||
&mut blocking(pool, move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
);
|
||||||
|
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
|
||||||
if !editors.contains(&user_id) {
|
if !editors.contains(&user_id) {
|
||||||
return Err(APIError::err("no_post_edit_allowed").into());
|
return Err(APIError::err("no_post_edit_allowed").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a community ban
|
// Check for a community ban
|
||||||
let community_id = data.community_id;
|
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
|
||||||
let is_banned =
|
|
||||||
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
|
|
||||||
if blocking(pool, is_banned).await? {
|
|
||||||
return Err(APIError::err("community_ban").into());
|
return Err(APIError::err("community_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
|
if UserView::read(&conn, user_id)?.banned {
|
||||||
if user.banned {
|
|
||||||
return Err(APIError::err("site_ban").into());
|
return Err(APIError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Iframely and Pictrs cached image
|
// Fetch Iframely and Pictshare cached image
|
||||||
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
|
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
|
||||||
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
|
fetch_iframely_and_pictshare_data(data.url.to_owned());
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let read_post = Post::read(&conn, data.edit_id)?;
|
||||||
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
|
|
||||||
|
|
||||||
let post_form = PostForm {
|
let post_form = PostForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -575,15 +395,12 @@ impl Perform for Oper<EditPost> {
|
||||||
embed_title: iframely_title,
|
embed_title: iframely_title,
|
||||||
embed_description: iframely_description,
|
embed_description: iframely_description,
|
||||||
embed_html: iframely_html,
|
embed_html: iframely_html,
|
||||||
thumbnail_url: pictrs_thumbnail,
|
thumbnail_url: pictshare_thumbnail,
|
||||||
ap_id: read_post.ap_id,
|
ap_id: read_post.ap_id,
|
||||||
local: read_post.local,
|
local: read_post.local,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
|
||||||
let res = blocking(pool, move |conn| Post::update(conn, edit_id, &post_form)).await?;
|
|
||||||
let updated_post: Post = match res {
|
|
||||||
Ok(post) => post,
|
Ok(post) => post,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_type = if e.to_string() == "value too long for type character varying(200)" {
|
let err_type = if e.to_string() == "value too long for type character varying(200)" {
|
||||||
|
@ -604,7 +421,7 @@ impl Perform for Oper<EditPost> {
|
||||||
removed: Some(removed),
|
removed: Some(removed),
|
||||||
reason: data.reason.to_owned(),
|
reason: data.reason.to_owned(),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
|
ModRemovePost::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(locked) = data.locked.to_owned() {
|
if let Some(locked) = data.locked.to_owned() {
|
||||||
|
@ -613,7 +430,7 @@ impl Perform for Oper<EditPost> {
|
||||||
post_id: data.edit_id,
|
post_id: data.edit_id,
|
||||||
locked: Some(locked),
|
locked: Some(locked),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
|
ModLockPost::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(stickied) = data.stickied.to_owned() {
|
if let Some(stickied) = data.stickied.to_owned() {
|
||||||
|
@ -622,58 +439,17 @@ impl Perform for Oper<EditPost> {
|
||||||
post_id: data.edit_id,
|
post_id: data.edit_id,
|
||||||
stickied: Some(stickied),
|
stickied: Some(stickied),
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
|
ModStickyPost::create(&conn, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(deleted) = data.deleted.to_owned() {
|
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
|
||||||
if deleted {
|
|
||||||
updated_post.send_delete(&user, &self.client, pool).await?;
|
|
||||||
} else {
|
|
||||||
updated_post
|
|
||||||
.send_undo_delete(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else if let Some(removed) = data.removed.to_owned() {
|
|
||||||
if removed {
|
|
||||||
updated_post.send_remove(&user, &self.client, pool).await?;
|
|
||||||
} else {
|
|
||||||
updated_post
|
|
||||||
.send_undo_remove(&user, &self.client, pool)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated_post.send_update(&user, &self.client, pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let edit_id = data.edit_id;
|
Ok(PostResponse { post: post_view })
|
||||||
let post_view = blocking(pool, move |conn| {
|
|
||||||
PostView::read(conn, edit_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PostResponse { post: post_view };
|
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendPost {
|
|
||||||
op: UserOperation::EditPost,
|
|
||||||
post: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<PostResponse> for Oper<SavePost> {
|
||||||
impl Perform for Oper<SavePost> {
|
fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
|
||||||
type Response = PostResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<PostResponse, LemmyError> {
|
|
||||||
let data: &SavePost = &self.data;
|
let data: &SavePost = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -689,22 +465,18 @@ impl Perform for Oper<SavePost> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if data.save {
|
if data.save {
|
||||||
let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
|
match PostSaved::save(&conn, &post_saved_form) {
|
||||||
if blocking(pool, save).await?.is_err() {
|
Ok(post) => post,
|
||||||
return Err(APIError::err("couldnt_save_post").into());
|
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
|
match PostSaved::unsave(&conn, &post_saved_form) {
|
||||||
if blocking(pool, unsave).await?.is_err() {
|
Ok(post) => post,
|
||||||
return Err(APIError::err("couldnt_save_post").into());
|
Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_id = data.post_id;
|
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
|
||||||
let post_view = blocking(pool, move |conn| {
|
|
||||||
PostView::read(conn, post_id, Some(user_id))
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(PostResponse { post: post_view })
|
Ok(PostResponse { post: post_view })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,9 @@
|
||||||
use super::user::Register;
|
use super::*;
|
||||||
use crate::{
|
use crate::api::user::Register;
|
||||||
api::{APIError, Oper, Perform},
|
use crate::api::{Oper, Perform};
|
||||||
apub::fetcher::search_by_apub_id,
|
use crate::settings::Settings;
|
||||||
blocking,
|
use diesel::PgConnection;
|
||||||
db::{
|
use log::info;
|
||||||
category::*,
|
|
||||||
comment_view::*,
|
|
||||||
community_view::*,
|
|
||||||
moderator::*,
|
|
||||||
moderator_views::*,
|
|
||||||
post_view::*,
|
|
||||||
site::*,
|
|
||||||
site_view::*,
|
|
||||||
user::*,
|
|
||||||
user_view::*,
|
|
||||||
Crud,
|
|
||||||
SearchType,
|
|
||||||
SortType,
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
settings::Settings,
|
|
||||||
slur_check,
|
|
||||||
slurs_vec_to_str,
|
|
||||||
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use log::{debug, info};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -38,7 +14,7 @@ pub struct ListCategoriesResponse {
|
||||||
categories: Vec<Category>,
|
categories: Vec<Category>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Search {
|
pub struct Search {
|
||||||
q: String,
|
q: String,
|
||||||
type_: String,
|
type_: String,
|
||||||
|
@ -49,13 +25,13 @@ pub struct Search {
|
||||||
auth: Option<String>,
|
auth: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SearchResponse {
|
pub struct SearchResponse {
|
||||||
pub type_: String,
|
type_: String,
|
||||||
pub comments: Vec<CommentView>,
|
comments: Vec<CommentView>,
|
||||||
pub posts: Vec<PostView>,
|
posts: Vec<PostView>,
|
||||||
pub communities: Vec<CommunityView>,
|
communities: Vec<CommunityView>,
|
||||||
pub users: Vec<UserView>,
|
users: Vec<UserView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -102,7 +78,7 @@ pub struct EditSite {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct GetSite {}
|
pub struct GetSite {}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SiteResponse {
|
pub struct SiteResponse {
|
||||||
site: SiteView,
|
site: SiteView,
|
||||||
}
|
}
|
||||||
|
@ -121,95 +97,71 @@ pub struct TransferSite {
|
||||||
auth: String,
|
auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
|
||||||
pub struct GetSiteConfig {
|
fn perform(&self, conn: &PgConnection) -> Result<ListCategoriesResponse, Error> {
|
||||||
auth: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct GetSiteConfigResponse {
|
|
||||||
config_hjson: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct SaveSiteConfig {
|
|
||||||
config_hjson: String,
|
|
||||||
auth: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for Oper<ListCategories> {
|
|
||||||
type Response = ListCategoriesResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<ListCategoriesResponse, LemmyError> {
|
|
||||||
let _data: &ListCategories = &self.data;
|
let _data: &ListCategories = &self.data;
|
||||||
|
|
||||||
let categories = blocking(pool, move |conn| Category::list_all(conn)).await??;
|
let categories: Vec<Category> = Category::list_all(&conn)?;
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(ListCategoriesResponse { categories })
|
Ok(ListCategoriesResponse { categories })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetModlogResponse> for Oper<GetModlog> {
|
||||||
impl Perform for Oper<GetModlog> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetModlogResponse, Error> {
|
||||||
type Response = GetModlogResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetModlogResponse, LemmyError> {
|
|
||||||
let data: &GetModlog = &self.data;
|
let data: &GetModlog = &self.data;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let removed_posts = ModRemovePostView::list(
|
||||||
let mod_user_id = data.mod_user_id;
|
&conn,
|
||||||
let page = data.page;
|
data.community_id,
|
||||||
let limit = data.limit;
|
data.mod_user_id,
|
||||||
let removed_posts = blocking(pool, move |conn| {
|
data.page,
|
||||||
ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
|
data.limit,
|
||||||
})
|
)?;
|
||||||
.await??;
|
let locked_posts = ModLockPostView::list(
|
||||||
|
&conn,
|
||||||
let locked_posts = blocking(pool, move |conn| {
|
data.community_id,
|
||||||
ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
|
data.mod_user_id,
|
||||||
})
|
data.page,
|
||||||
.await??;
|
data.limit,
|
||||||
|
)?;
|
||||||
let stickied_posts = blocking(pool, move |conn| {
|
let stickied_posts = ModStickyPostView::list(
|
||||||
ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
|
&conn,
|
||||||
})
|
data.community_id,
|
||||||
.await??;
|
data.mod_user_id,
|
||||||
|
data.page,
|
||||||
let removed_comments = blocking(pool, move |conn| {
|
data.limit,
|
||||||
ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
|
)?;
|
||||||
})
|
let removed_comments = ModRemoveCommentView::list(
|
||||||
.await??;
|
&conn,
|
||||||
|
data.community_id,
|
||||||
let banned_from_community = blocking(pool, move |conn| {
|
data.mod_user_id,
|
||||||
ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
|
data.page,
|
||||||
})
|
data.limit,
|
||||||
.await??;
|
)?;
|
||||||
|
let banned_from_community = ModBanFromCommunityView::list(
|
||||||
let added_to_community = blocking(pool, move |conn| {
|
&conn,
|
||||||
ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
|
data.community_id,
|
||||||
})
|
data.mod_user_id,
|
||||||
.await??;
|
data.page,
|
||||||
|
data.limit,
|
||||||
|
)?;
|
||||||
|
let added_to_community = ModAddCommunityView::list(
|
||||||
|
&conn,
|
||||||
|
data.community_id,
|
||||||
|
data.mod_user_id,
|
||||||
|
data.page,
|
||||||
|
data.limit,
|
||||||
|
)?;
|
||||||
|
|
||||||
// These arrays are only for the full modlog, when a community isn't given
|
// 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() {
|
let (removed_communities, banned, added) = if data.community_id.is_none() {
|
||||||
blocking(pool, move |conn| {
|
(
|
||||||
Ok((
|
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
|
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
ModBanView::list(conn, mod_user_id, page, limit)?,
|
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?,
|
||||||
ModAddView::list(conn, mod_user_id, page, limit)?,
|
)
|
||||||
)) as Result<_, LemmyError>
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
} else {
|
} else {
|
||||||
(Vec::new(), Vec::new(), Vec::new())
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
};
|
};
|
||||||
|
@ -229,15 +181,8 @@ impl Perform for Oper<GetModlog> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<SiteResponse> for Oper<CreateSite> {
|
||||||
impl Perform for Oper<CreateSite> {
|
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||||
type Response = SiteResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<SiteResponse, LemmyError> {
|
|
||||||
let data: &CreateSite = &self.data;
|
let data: &CreateSite = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -258,8 +203,7 @@ impl Perform for Oper<CreateSite> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
if !user.admin {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,25 +217,19 @@ impl Perform for Oper<CreateSite> {
|
||||||
updated: None,
|
updated: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
|
match Site::create(&conn, &site_form) {
|
||||||
if blocking(pool, create_site).await?.is_err() {
|
Ok(site) => site,
|
||||||
return Err(APIError::err("site_already_exists").into());
|
Err(_e) => return Err(APIError::err("site_already_exists").into()),
|
||||||
}
|
};
|
||||||
|
|
||||||
let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
Ok(SiteResponse { site: site_view })
|
Ok(SiteResponse { site: site_view })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<SiteResponse> for Oper<EditSite> {
|
||||||
impl Perform for Oper<EditSite> {
|
fn perform(&self, conn: &PgConnection) -> Result<SiteResponse, Error> {
|
||||||
type Response = SiteResponse;
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<SiteResponse, LemmyError> {
|
|
||||||
let data: &EditSite = &self.data;
|
let data: &EditSite = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -312,12 +250,11 @@ impl Perform for Oper<EditSite> {
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
|
if !UserView::read(&conn, user_id)?.admin {
|
||||||
if !user.admin {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let found_site = Site::read(&conn, 1)?;
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
|
@ -329,42 +266,24 @@ impl Perform for Oper<EditSite> {
|
||||||
enable_nsfw: data.enable_nsfw,
|
enable_nsfw: data.enable_nsfw,
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
|
match Site::update(&conn, 1, &site_form) {
|
||||||
if blocking(pool, update_site).await?.is_err() {
|
Ok(site) => site,
|
||||||
return Err(APIError::err("couldnt_update_site").into());
|
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||||
}
|
};
|
||||||
|
|
||||||
let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
let res = SiteResponse { site: site_view };
|
Ok(SiteResponse { site: site_view })
|
||||||
|
|
||||||
if let Some(ws) = websocket_info {
|
|
||||||
ws.chatserver.do_send(SendAllMessage {
|
|
||||||
op: UserOperation::EditSite,
|
|
||||||
response: res.clone(),
|
|
||||||
my_id: ws.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetSiteResponse> for Oper<GetSite> {
|
||||||
impl Perform for Oper<GetSite> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||||
type Response = GetSiteResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetSiteResponse, LemmyError> {
|
|
||||||
let _data: &GetSite = &self.data;
|
let _data: &GetSite = &self.data;
|
||||||
|
|
||||||
// TODO refactor this a little
|
let site = Site::read(&conn, 1);
|
||||||
let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
|
let site_view = if site.is_ok() {
|
||||||
let site_view = if res.is_ok() {
|
Some(SiteView::read(&conn)?)
|
||||||
Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
|
|
||||||
} else if let Some(setup) = Settings::get().setup.as_ref() {
|
} else if let Some(setup) = Settings::get().setup.as_ref() {
|
||||||
let register = Register {
|
let register = Register {
|
||||||
username: setup.admin_username.to_owned(),
|
username: setup.admin_username.to_owned(),
|
||||||
|
@ -374,81 +293,47 @@ impl Perform for Oper<GetSite> {
|
||||||
admin: true,
|
admin: true,
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
};
|
};
|
||||||
let login_response = Oper::new(register, self.client.clone())
|
let login_response = Oper::new(register).perform(&conn)?;
|
||||||
.perform(pool, websocket_info.clone())
|
|
||||||
.await?;
|
|
||||||
info!("Admin {} created", setup.admin_username);
|
info!("Admin {} created", setup.admin_username);
|
||||||
|
|
||||||
let create_site = CreateSite {
|
let create_site = CreateSite {
|
||||||
name: setup.site_name.to_owned(),
|
name: setup.site_name.to_owned(),
|
||||||
description: None,
|
description: None,
|
||||||
enable_downvotes: true,
|
enable_downvotes: false,
|
||||||
open_registration: true,
|
open_registration: false,
|
||||||
enable_nsfw: true,
|
enable_nsfw: false,
|
||||||
auth: login_response.jwt,
|
auth: login_response.jwt,
|
||||||
};
|
};
|
||||||
Oper::new(create_site, self.client.clone())
|
Oper::new(create_site).perform(&conn)?;
|
||||||
.perform(pool, websocket_info.clone())
|
|
||||||
.await?;
|
|
||||||
info!("Site {} created", setup.site_name);
|
info!("Site {} created", setup.site_name);
|
||||||
Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
|
Some(SiteView::read(&conn)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
let mut admins = UserView::admins(&conn)?;
|
||||||
|
if site_view.is_some() {
|
||||||
// Make sure the site creator is the top admin
|
let site_creator_id = site_view.to_owned().unwrap().creator_id;
|
||||||
if let Some(site_view) = site_view.to_owned() {
|
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
|
||||||
let site_creator_id = site_view.creator_id;
|
let creator_user = admins.remove(creator_index);
|
||||||
// TODO investigate why this is sometimes coming back null
|
admins.insert(0, creator_user);
|
||||||
// Maybe user_.admin isn't being set to true?
|
|
||||||
if let Some(creator_index) = admins.iter().position(|r| r.id == site_creator_id) {
|
|
||||||
let creator_user = admins.remove(creator_index);
|
|
||||||
admins.insert(0, creator_user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
|
let banned = UserView::banned(&conn)?;
|
||||||
|
|
||||||
let online = if let Some(_ws) = websocket_info {
|
|
||||||
// TODO
|
|
||||||
1
|
|
||||||
// let fut = async {
|
|
||||||
// ws.chatserver.send(GetUsersOnline).await.unwrap()
|
|
||||||
// };
|
|
||||||
// Runtime::new().unwrap().block_on(fut)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
site: site_view,
|
site: site_view,
|
||||||
admins,
|
admins,
|
||||||
banned,
|
banned,
|
||||||
online,
|
online: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<SearchResponse> for Oper<Search> {
|
||||||
impl Perform for Oper<Search> {
|
fn perform(&self, conn: &PgConnection) -> Result<SearchResponse, Error> {
|
||||||
type Response = SearchResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<SearchResponse, LemmyError> {
|
|
||||||
let data: &Search = &self.data;
|
let data: &Search = &self.data;
|
||||||
|
|
||||||
dbg!(&data);
|
|
||||||
|
|
||||||
match search_by_apub_id(&data.q, &self.client, pool).await {
|
|
||||||
Ok(r) => return Ok(r),
|
|
||||||
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_id: Option<i32> = match &data.auth {
|
let user_id: Option<i32> = match &data.auth {
|
||||||
Some(auth) => match Claims::decode(&auth) {
|
Some(auth) => match Claims::decode(&auth) {
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
|
@ -460,6 +345,7 @@ impl Perform for Oper<Search> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sort = SortType::from_str(&data.sort)?;
|
||||||
let type_ = SearchType::from_str(&data.type_)?;
|
let type_ = SearchType::from_str(&data.type_)?;
|
||||||
|
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
|
@ -469,126 +355,85 @@ impl Perform for Oper<Search> {
|
||||||
|
|
||||||
// TODO no clean / non-nsfw searching rn
|
// 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;
|
|
||||||
match type_ {
|
match type_ {
|
||||||
SearchType::Posts => {
|
SearchType::Posts => {
|
||||||
posts = blocking(pool, move |conn| {
|
posts = PostQueryBuilder::create(&conn)
|
||||||
PostQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.show_nsfw(true)
|
||||||
.show_nsfw(true)
|
.for_community_id(data.community_id)
|
||||||
.for_community_id(community_id)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.my_user_id(user_id)
|
||||||
.my_user_id(user_id)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
SearchType::Comments => {
|
SearchType::Comments => {
|
||||||
comments = blocking(pool, move |conn| {
|
comments = CommentQueryBuilder::create(&conn)
|
||||||
CommentQueryBuilder::create(&conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.my_user_id(user_id)
|
||||||
.my_user_id(user_id)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
SearchType::Communities => {
|
SearchType::Communities => {
|
||||||
communities = blocking(pool, move |conn| {
|
communities = CommunityQueryBuilder::create(&conn)
|
||||||
CommunityQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
SearchType::Users => {
|
SearchType::Users => {
|
||||||
users = blocking(pool, move |conn| {
|
users = UserQueryBuilder::create(&conn)
|
||||||
UserQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
SearchType::All => {
|
SearchType::All => {
|
||||||
posts = blocking(pool, move |conn| {
|
posts = PostQueryBuilder::create(&conn)
|
||||||
PostQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.show_nsfw(true)
|
||||||
.show_nsfw(true)
|
.for_community_id(data.community_id)
|
||||||
.for_community_id(community_id)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.my_user_id(user_id)
|
||||||
.my_user_id(user_id)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let q = data.q.to_owned();
|
comments = CommentQueryBuilder::create(&conn)
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
.sort(&sort)
|
||||||
|
.search_term(data.q.to_owned())
|
||||||
|
.my_user_id(user_id)
|
||||||
|
.page(data.page)
|
||||||
|
.limit(data.limit)
|
||||||
|
.list()?;
|
||||||
|
|
||||||
comments = blocking(pool, move |conn| {
|
communities = CommunityQueryBuilder::create(&conn)
|
||||||
CommentQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.search_term(data.q.to_owned())
|
||||||
.search_term(q)
|
.page(data.page)
|
||||||
.my_user_id(user_id)
|
.limit(data.limit)
|
||||||
.page(page)
|
.list()?;
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let q = data.q.to_owned();
|
users = UserQueryBuilder::create(&conn)
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
.sort(&sort)
|
||||||
|
.search_term(data.q.to_owned())
|
||||||
communities = blocking(pool, move |conn| {
|
.page(data.page)
|
||||||
CommunityQueryBuilder::create(conn)
|
.limit(data.limit)
|
||||||
.sort(&sort)
|
.list()?;
|
||||||
.search_term(q)
|
|
||||||
.page(page)
|
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let q = data.q.to_owned();
|
|
||||||
let sort = SortType::from_str(&data.sort)?;
|
|
||||||
|
|
||||||
users = blocking(pool, move |conn| {
|
|
||||||
UserQueryBuilder::create(conn)
|
|
||||||
.sort(&sort)
|
|
||||||
.search_term(q)
|
|
||||||
.page(page)
|
|
||||||
.limit(limit)
|
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
SearchType::Url => {
|
SearchType::Url => {
|
||||||
posts = blocking(pool, move |conn| {
|
posts = PostQueryBuilder::create(&conn)
|
||||||
PostQueryBuilder::create(conn)
|
.sort(&sort)
|
||||||
.sort(&sort)
|
.show_nsfw(true)
|
||||||
.show_nsfw(true)
|
.for_community_id(data.community_id)
|
||||||
.for_community_id(community_id)
|
.url_search(data.q.to_owned())
|
||||||
.url_search(q)
|
.page(data.page)
|
||||||
.page(page)
|
.limit(data.limit)
|
||||||
.limit(limit)
|
.list()?;
|
||||||
.list()
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -603,15 +448,8 @@ impl Perform for Oper<Search> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Perform<GetSiteResponse> for Oper<TransferSite> {
|
||||||
impl Perform for Oper<TransferSite> {
|
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
|
||||||
type Response = GetSiteResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetSiteResponse, LemmyError> {
|
|
||||||
let data: &TransferSite = &self.data;
|
let data: &TransferSite = &self.data;
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
let claims = match Claims::decode(&data.auth) {
|
||||||
|
@ -621,7 +459,7 @@ impl Perform for Oper<TransferSite> {
|
||||||
|
|
||||||
let user_id = claims.id;
|
let user_id = claims.id;
|
||||||
|
|
||||||
let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
let read_site = Site::read(&conn, 1)?;
|
||||||
|
|
||||||
// Make sure user is the creator
|
// Make sure user is the creator
|
||||||
if read_site.creator_id != user_id {
|
if read_site.creator_id != user_id {
|
||||||
|
@ -638,9 +476,9 @@ impl Perform for Oper<TransferSite> {
|
||||||
enable_nsfw: read_site.enable_nsfw,
|
enable_nsfw: read_site.enable_nsfw,
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
|
match Site::update(&conn, 1, &site_form) {
|
||||||
if blocking(pool, update_site).await?.is_err() {
|
Ok(site) => site,
|
||||||
return Err(APIError::err("couldnt_update_site").into());
|
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mod tables
|
// Mod tables
|
||||||
|
@ -650,11 +488,11 @@ impl Perform for Oper<TransferSite> {
|
||||||
removed: Some(false),
|
removed: Some(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
blocking(pool, move |conn| ModAdd::create(conn, &form)).await??;
|
ModAdd::create(&conn, &form)?;
|
||||||
|
|
||||||
let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
|
let site_view = SiteView::read(&conn)?;
|
||||||
|
|
||||||
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
let mut admins = UserView::admins(&conn)?;
|
||||||
let creator_index = admins
|
let creator_index = admins
|
||||||
.iter()
|
.iter()
|
||||||
.position(|r| r.id == site_view.creator_id)
|
.position(|r| r.id == site_view.creator_id)
|
||||||
|
@ -662,7 +500,7 @@ impl Perform for Oper<TransferSite> {
|
||||||
let creator_user = admins.remove(creator_index);
|
let creator_user = admins.remove(creator_index);
|
||||||
admins.insert(0, creator_user);
|
admins.insert(0, creator_user);
|
||||||
|
|
||||||
let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
|
let banned = UserView::banned(&conn)?;
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
Ok(GetSiteResponse {
|
||||||
site: Some(site_view),
|
site: Some(site_view),
|
||||||
|
@ -672,71 +510,3 @@ impl Perform for Oper<TransferSite> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for Oper<GetSiteConfig> {
|
|
||||||
type Response = GetSiteConfigResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetSiteConfigResponse, LemmyError> {
|
|
||||||
let data: &GetSiteConfig = &self.data;
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
|
||||||
Ok(claims) => claims.claims,
|
|
||||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_id = claims.id;
|
|
||||||
|
|
||||||
// Only let admins read this
|
|
||||||
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
|
|
||||||
|
|
||||||
if !admin_ids.contains(&user_id) {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_hjson = Settings::read_config_file()?;
|
|
||||||
|
|
||||||
Ok(GetSiteConfigResponse { config_hjson })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for Oper<SaveSiteConfig> {
|
|
||||||
type Response = GetSiteConfigResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
pool: &DbPool,
|
|
||||||
_websocket_info: Option<WebsocketInfo>,
|
|
||||||
) -> Result<GetSiteConfigResponse, LemmyError> {
|
|
||||||
let data: &SaveSiteConfig = &self.data;
|
|
||||||
|
|
||||||
let claims = match Claims::decode(&data.auth) {
|
|
||||||
Ok(claims) => claims.claims,
|
|
||||||
Err(_e) => return Err(APIError::err("not_logged_in").into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_id = claims.id;
|
|
||||||
|
|
||||||
// Only let admins read this
|
|
||||||
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
|
|
||||||
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
|
|
||||||
|
|
||||||
if !admin_ids.contains(&user_id) {
|
|
||||||
return Err(APIError::err("not_an_admin").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,88 +0,0 @@
|
||||||
use crate::{
|
|
||||||
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
|
|
||||||
db::{activity::insert_activity, community::Community, user::User_},
|
|
||||||
request::retry_custom,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
|
|
||||||
use actix_web::client::Client;
|
|
||||||
use log::debug;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub fn populate_object_props(
|
|
||||||
props: &mut ObjectProperties,
|
|
||||||
addressed_ccs: Vec<String>,
|
|
||||||
object_id: &str,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
// TODO: the activity needs a seperate id from the object
|
|
||||||
.set_id(object_id)?
|
|
||||||
// TODO: should to/cc go on the Create, or on the Post? or on both?
|
|
||||||
// TODO: handle privacy on the receiving side (at least ignore anything thats not public)
|
|
||||||
.set_to_xsd_any_uri(public())?
|
|
||||||
.set_many_cc_xsd_any_uris(addressed_ccs)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_activity_to_community<A>(
|
|
||||||
creator: &User_,
|
|
||||||
community: &Community,
|
|
||||||
to: Vec<String>,
|
|
||||||
activity: A,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
|
|
||||||
{
|
|
||||||
insert_activity(creator.id, activity.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
// if this is a local community, we need to do an announce from the community instead
|
|
||||||
if community.local {
|
|
||||||
Community::do_announce(activity, &community, creator, client, pool).await?;
|
|
||||||
} else {
|
|
||||||
send_activity(client, &activity, creator, to).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send an activity to a list of recipients, using the correct headers etc.
|
|
||||||
pub async fn send_activity<A>(
|
|
||||||
client: &Client,
|
|
||||||
activity: &A,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
to: Vec<String>,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
A: Serialize,
|
|
||||||
{
|
|
||||||
let activity = serde_json::to_string(&activity)?;
|
|
||||||
debug!("Sending activitypub activity {} to {:?}", activity, to);
|
|
||||||
|
|
||||||
for t in to {
|
|
||||||
let to_url = Url::parse(&t)?;
|
|
||||||
if !is_apub_id_valid(&to_url) {
|
|
||||||
debug!("Not sending activity to {} (invalid or blocklisted)", t);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = retry_custom(|| async {
|
|
||||||
let request = client.post(&t).header("Content-Type", "application/json");
|
|
||||||
|
|
||||||
match sign(request, actor, activity.clone()).await {
|
|
||||||
Ok(signed) => Ok(signed.send().await),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
debug!("Result for activity send: {:?}", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,633 +0,0 @@
|
||||||
use crate::{
|
|
||||||
apub::{
|
|
||||||
activities::{populate_object_props, send_activity_to_community},
|
|
||||||
create_apub_response,
|
|
||||||
create_apub_tombstone_response,
|
|
||||||
create_tombstone,
|
|
||||||
fetch_webfinger_url,
|
|
||||||
fetcher::{
|
|
||||||
get_or_fetch_and_insert_remote_comment,
|
|
||||||
get_or_fetch_and_insert_remote_post,
|
|
||||||
get_or_fetch_and_upsert_remote_user,
|
|
||||||
},
|
|
||||||
ActorType,
|
|
||||||
ApubLikeableType,
|
|
||||||
ApubObjectType,
|
|
||||||
FromApub,
|
|
||||||
ToApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
convert_datetime,
|
|
||||||
db::{
|
|
||||||
comment::{Comment, CommentForm},
|
|
||||||
community::Community,
|
|
||||||
post::Post,
|
|
||||||
user::User_,
|
|
||||||
Crud,
|
|
||||||
},
|
|
||||||
routes::DbPoolParam,
|
|
||||||
scrape_text_for_mentions,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
MentionData,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
|
||||||
context,
|
|
||||||
link::Mention,
|
|
||||||
object::{kind::NoteType, properties::ObjectProperties, Note},
|
|
||||||
};
|
|
||||||
use activitystreams_new::object::Tombstone;
|
|
||||||
use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use log::debug;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct CommentQuery {
|
|
||||||
comment_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the post json over HTTP.
|
|
||||||
pub async fn get_apub_comment(
|
|
||||||
info: Path<CommentQuery>,
|
|
||||||
db: DbPoolParam,
|
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
|
||||||
let id = info.comment_id.parse::<i32>()?;
|
|
||||||
let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??;
|
|
||||||
|
|
||||||
if !comment.deleted {
|
|
||||||
Ok(create_apub_response(&comment.to_apub(&db).await?))
|
|
||||||
} else {
|
|
||||||
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ToApub for Comment {
|
|
||||||
type Response = Note;
|
|
||||||
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
|
||||||
let mut comment = Note::default();
|
|
||||||
let oprops: &mut ObjectProperties = comment.as_mut();
|
|
||||||
|
|
||||||
let creator_id = self.creator_id;
|
|
||||||
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_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];
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
oprops
|
|
||||||
// Not needed when the Post is embedded in a collection (like for community outbox)
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(self.ap_id.to_owned())?
|
|
||||||
.set_published(convert_datetime(self.published))?
|
|
||||||
.set_to_xsd_any_uri(community.actor_id)?
|
|
||||||
.set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
|
|
||||||
.set_content_xsd_string(self.content.to_owned())?
|
|
||||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
|
||||||
|
|
||||||
if let Some(u) = self.updated {
|
|
||||||
oprops.set_updated(convert_datetime(u))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
create_tombstone(
|
|
||||||
self.deleted,
|
|
||||||
&self.ap_id,
|
|
||||||
self.updated,
|
|
||||||
NoteType.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for CommentForm {
|
|
||||||
type ApubType = Note;
|
|
||||||
|
|
||||||
/// Parse an ActivityPub note received from another instance into a Lemmy comment
|
|
||||||
async fn from_apub(
|
|
||||||
note: &Note,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<CommentForm, LemmyError> {
|
|
||||||
let oprops = ¬e.object_props;
|
|
||||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
|
|
||||||
|
|
||||||
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
|
|
||||||
let post_ap_id = in_reply_tos.next().unwrap().to_string();
|
|
||||||
|
|
||||||
// This post, or the parent comment might not yet exist on this server yet, fetch them.
|
|
||||||
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).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<i32> = match in_reply_tos.next() {
|
|
||||||
Some(parent_comment_uri) => {
|
|
||||||
let parent_comment_ap_id = &parent_comment_uri.to_string();
|
|
||||||
let parent_comment =
|
|
||||||
get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?;
|
|
||||||
|
|
||||||
Some(parent_comment.id)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(CommentForm {
|
|
||||||
creator_id: creator.id,
|
|
||||||
post_id: post.id,
|
|
||||||
parent_id,
|
|
||||||
content: oprops
|
|
||||||
.get_content_xsd_string()
|
|
||||||
.map(|c| c.to_string())
|
|
||||||
.unwrap(),
|
|
||||||
removed: None,
|
|
||||||
read: None,
|
|
||||||
published: oprops
|
|
||||||
.get_published()
|
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
|
||||||
updated: oprops
|
|
||||||
.get_updated()
|
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
|
||||||
deleted: None,
|
|
||||||
ap_id: oprops.get_id().unwrap().to_string(),
|
|
||||||
local: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ApubObjectType for Comment {
|
|
||||||
/// Send out information about a newly created comment, to the followers of the community.
|
|
||||||
async fn send_create(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let maa =
|
|
||||||
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut create = Create::new();
|
|
||||||
populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
|
|
||||||
|
|
||||||
// Set the mention tags
|
|
||||||
create.object_props.set_many_tag_base_boxes(maa.tags)?;
|
|
||||||
|
|
||||||
create
|
|
||||||
.create_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send out information about an edited post, to the followers of the community.
|
|
||||||
async fn send_update(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let maa =
|
|
||||||
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut update = Update::new();
|
|
||||||
populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
|
|
||||||
|
|
||||||
// Set the mention tags
|
|
||||||
update.object_props.set_many_tag_base_boxes(maa.tags)?;
|
|
||||||
|
|
||||||
update
|
|
||||||
.update_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
delete,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
// Generate a fake delete activity, with the correct object
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(delete)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&mod_,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
remove,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
// Generate a fake delete activity, with the correct object
|
|
||||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(remove)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&mod_,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ApubLikeableType for Comment {
|
|
||||||
async fn send_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut like = Like::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut like.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
like
|
|
||||||
.like_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
like,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_dislike(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut dislike = Dislike::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut dislike.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
dislike
|
|
||||||
.dislike_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
dislike,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let post_id = self.post_id;
|
|
||||||
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
|
|
||||||
|
|
||||||
let community_id = post.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut like = Like::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut like.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
like
|
|
||||||
.like_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(like)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MentionsAndAddresses {
|
|
||||||
addressed_ccs: Vec<String>,
|
|
||||||
inboxes: Vec<String>,
|
|
||||||
tags: Vec<Mention>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 users / addresses that go in the cc field.
|
|
||||||
async fn collect_non_local_mentions_and_addresses(
|
|
||||||
content: &str,
|
|
||||||
community: &Community,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<MentionsAndAddresses, LemmyError> {
|
|
||||||
let mut addressed_ccs = vec![community.get_followers_url()];
|
|
||||||
|
|
||||||
// Add the mention tag
|
|
||||||
let mut tags = Vec::new();
|
|
||||||
|
|
||||||
// Get the inboxes for any mentions
|
|
||||||
let mentions = scrape_text_for_mentions(&content)
|
|
||||||
.into_iter()
|
|
||||||
// Filter only the non-local ones
|
|
||||||
.filter(|m| !m.is_local())
|
|
||||||
.collect::<Vec<MentionData>>();
|
|
||||||
|
|
||||||
let mut mention_inboxes = Vec::new();
|
|
||||||
for mention in &mentions {
|
|
||||||
// TODO should it be fetching it every time?
|
|
||||||
if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
|
|
||||||
debug!("mention actor_id: {}", actor_id);
|
|
||||||
addressed_ccs.push(actor_id.to_owned());
|
|
||||||
|
|
||||||
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
|
|
||||||
let shared_inbox = mention_user.get_shared_inbox_url();
|
|
||||||
|
|
||||||
mention_inboxes.push(shared_inbox);
|
|
||||||
let mut mention_tag = Mention::new();
|
|
||||||
mention_tag
|
|
||||||
.link_props
|
|
||||||
.set_href(actor_id)?
|
|
||||||
.set_name_xsd_string(mention.full_name())?;
|
|
||||||
tags.push(mention_tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inboxes = vec![community.get_shared_inbox_url()];
|
|
||||||
inboxes.extend(mention_inboxes);
|
|
||||||
inboxes = inboxes.into_iter().unique().collect();
|
|
||||||
|
|
||||||
Ok(MentionsAndAddresses {
|
|
||||||
addressed_ccs,
|
|
||||||
inboxes,
|
|
||||||
tags,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,488 +1,192 @@
|
||||||
use crate::{
|
use crate::apub::puller::fetch_remote_object;
|
||||||
apub::{
|
use crate::apub::*;
|
||||||
activities::{populate_object_props, send_activity},
|
use crate::convert_datetime;
|
||||||
create_apub_response,
|
use crate::db::community::Community;
|
||||||
create_apub_tombstone_response,
|
use crate::db::community_view::{CommunityFollowerView, CommunityView};
|
||||||
create_tombstone,
|
use crate::db::establish_unpooled_connection;
|
||||||
extensions::{group_extensions::GroupExtension, signatures::PublicKey},
|
use crate::db::post::Post;
|
||||||
fetcher::get_or_fetch_and_upsert_remote_user,
|
use crate::settings::Settings;
|
||||||
get_shared_inbox,
|
use activitystreams::actor::properties::ApActorProperties;
|
||||||
ActorType,
|
use activitystreams::collection::OrderedCollection;
|
||||||
FromApub,
|
|
||||||
GroupExt,
|
|
||||||
ToApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
convert_datetime,
|
|
||||||
db::{
|
|
||||||
activity::insert_activity,
|
|
||||||
community::{Community, CommunityForm},
|
|
||||||
community_view::{CommunityFollowerView, CommunityModeratorView},
|
|
||||||
user::User_,
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
routes::DbPoolParam,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::{Accept, Announce, Delete, Remove, Undo},
|
actor::Group, collection::UnorderedCollection, context, ext::Extensible,
|
||||||
actor::{kind::GroupType, properties::ApActorProperties, Group},
|
|
||||||
collection::UnorderedCollection,
|
|
||||||
context,
|
|
||||||
endpoint::EndpointProperties,
|
|
||||||
object::properties::ObjectProperties,
|
object::properties::ObjectProperties,
|
||||||
Activity,
|
|
||||||
Base,
|
|
||||||
BaseBox,
|
|
||||||
};
|
};
|
||||||
use activitystreams_ext::Ext3;
|
use actix_web::body::Body;
|
||||||
use activitystreams_new::{activity::Follow, object::Tombstone};
|
use actix_web::web::Path;
|
||||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
use actix_web::HttpResponse;
|
||||||
use itertools::Itertools;
|
use actix_web::{web, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
use std::fmt::Debug;
|
use diesel::PgConnection;
|
||||||
|
use failure::Error;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CommunityQuery {
|
pub struct CommunityQuery {
|
||||||
community_name: String,
|
community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
pub async fn get_apub_community_list(
|
||||||
impl ToApub for Community {
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
type Response = GroupExt;
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
|
// TODO: implement pagination
|
||||||
|
let communities = Community::list(&db.get().unwrap())?
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.as_group())
|
||||||
|
.collect::<Result<Vec<GroupExt>, Error>>()?;
|
||||||
|
let mut collection = UnorderedCollection::default();
|
||||||
|
let oprops: &mut ObjectProperties = collection.as_mut();
|
||||||
|
oprops.set_context_xsd_any_uri(context())?.set_id(format!(
|
||||||
|
"{}://{}/federation/communities",
|
||||||
|
get_apub_protocol_string(),
|
||||||
|
Settings::get().hostname
|
||||||
|
))?;
|
||||||
|
|
||||||
|
collection
|
||||||
|
.collection_props
|
||||||
|
.set_total_items(communities.len() as u64)?
|
||||||
|
.set_many_items_base_boxes(communities)?;
|
||||||
|
Ok(create_apub_response(&collection))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Community {
|
||||||
|
fn as_group(&self) -> Result<GroupExt, Error> {
|
||||||
|
let base_url = make_apub_endpoint(EndpointType::Community, &self.name);
|
||||||
|
|
||||||
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
|
|
||||||
let mut group = Group::default();
|
let mut group = Group::default();
|
||||||
let oprops: &mut ObjectProperties = group.as_mut();
|
let oprops: &mut ObjectProperties = group.as_mut();
|
||||||
|
|
||||||
// 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 = moderators.into_iter().map(|m| m.user_actor_id).collect();
|
|
||||||
|
|
||||||
oprops
|
oprops
|
||||||
.set_context_xsd_any_uri(context())?
|
.set_context_xsd_any_uri(context())?
|
||||||
.set_id(self.actor_id.to_owned())?
|
.set_id(base_url.to_owned())?
|
||||||
.set_name_xsd_string(self.name.to_owned())?
|
.set_name_xsd_string(self.name.to_owned())?
|
||||||
.set_published(convert_datetime(self.published))?
|
.set_published(convert_datetime(self.published))?
|
||||||
.set_many_attributed_to_xsd_any_uris(moderators)?;
|
.set_attributed_to_xsd_any_uri(make_apub_endpoint(
|
||||||
|
EndpointType::User,
|
||||||
|
&self.creator_id.to_string(),
|
||||||
|
))?;
|
||||||
|
|
||||||
if let Some(u) = self.updated.to_owned() {
|
if let Some(u) = self.updated.to_owned() {
|
||||||
oprops.set_updated(convert_datetime(u))?;
|
oprops.set_updated(convert_datetime(u))?;
|
||||||
}
|
}
|
||||||
if let Some(d) = self.description.to_owned() {
|
if let Some(d) = self.description.to_owned() {
|
||||||
// TODO: this should be html, also add source field with raw markdown
|
oprops.set_summary_xsd_string(d)?;
|
||||||
// -> same for post.content and others
|
|
||||||
oprops.set_content_xsd_string(d)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut endpoint_props = EndpointProperties::default();
|
|
||||||
|
|
||||||
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
|
|
||||||
|
|
||||||
let mut actor_props = ApActorProperties::default();
|
let mut actor_props = ApActorProperties::default();
|
||||||
|
|
||||||
actor_props
|
actor_props
|
||||||
.set_preferred_username(self.title.to_owned())?
|
.set_preferred_username(self.title.to_owned())?
|
||||||
.set_inbox(self.get_inbox_url())?
|
.set_inbox(format!("{}/inbox", &base_url))?
|
||||||
.set_outbox(self.get_outbox_url())?
|
.set_outbox(format!("{}/outbox", &base_url))?
|
||||||
.set_endpoints(endpoint_props)?
|
.set_followers(format!("{}/followers", &base_url))?;
|
||||||
.set_followers(self.get_followers_url())?;
|
|
||||||
|
|
||||||
let nsfw = self.nsfw;
|
Ok(group.extend(actor_props))
|
||||||
let category_id = self.category_id;
|
|
||||||
let group_extension = blocking(pool, move |conn| {
|
|
||||||
GroupExtension::new(conn, category_id, nsfw)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(Ext3::new(
|
|
||||||
group,
|
|
||||||
group_extension,
|
|
||||||
actor_props,
|
|
||||||
self.get_public_key_ext(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
create_tombstone(
|
|
||||||
self.deleted,
|
|
||||||
&self.actor_id,
|
|
||||||
self.updated,
|
|
||||||
GroupType.to_string(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl CommunityView {
|
||||||
impl ActorType for Community {
|
pub fn from_group(group: &GroupExt, domain: &str) -> Result<CommunityView, Error> {
|
||||||
fn actor_id(&self) -> String {
|
let followers_uri = &group.extension.get_followers().unwrap().to_string();
|
||||||
self.actor_id.to_owned()
|
let outbox_uri = &group.extension.get_outbox().to_string();
|
||||||
}
|
let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
|
||||||
|
let followers = fetch_remote_object::<UnorderedCollection>(followers_uri)?;
|
||||||
fn public_key(&self) -> String {
|
let oprops = &group.base.object_props;
|
||||||
self.public_key.to_owned().unwrap()
|
let aprops = &group.extension;
|
||||||
}
|
Ok(CommunityView {
|
||||||
fn private_key(&self) -> String {
|
// TODO: we need to merge id and name into a single thing (stuff like @user@instance.com)
|
||||||
self.private_key.to_owned().unwrap()
|
id: 1337, //community.object_props.get_id()
|
||||||
}
|
name: format_community_name(&oprops.get_name_xsd_string().unwrap().to_string(), domain),
|
||||||
|
|
||||||
/// As a local community, accept the follow request from a remote user.
|
|
||||||
async fn send_accept_follow(
|
|
||||||
&self,
|
|
||||||
follow: &Follow,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
|
|
||||||
let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut accept = Accept::new();
|
|
||||||
accept
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(id)?;
|
|
||||||
accept
|
|
||||||
.accept_props
|
|
||||||
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
|
|
||||||
let to = format!("{}/inbox", actor_uri);
|
|
||||||
|
|
||||||
insert_activity(self.creator_id, accept.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &accept, self, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let group = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
|
||||||
|
|
||||||
insert_activity(self.creator_id, delete.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
let inboxes = self.get_follower_inboxes(pool).await?;
|
|
||||||
|
|
||||||
// Note: For an accept, since it was automatic, no one pushed a button,
|
|
||||||
// the community was the actor.
|
|
||||||
// But for delete, the creator is the actor, and does the signing
|
|
||||||
send_activity(client, &delete, creator, inboxes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let group = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(delete)?;
|
|
||||||
|
|
||||||
insert_activity(self.creator_id, undo.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
let inboxes = self.get_follower_inboxes(pool).await?;
|
|
||||||
|
|
||||||
// Note: For an accept, since it was automatic, no one pushed a button,
|
|
||||||
// the community was the actor.
|
|
||||||
// But for delete, the creator is the actor, and does the signing
|
|
||||||
send_activity(client, &undo, creator, inboxes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let group = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
|
||||||
|
|
||||||
insert_activity(mod_.id, remove.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
let inboxes = self.get_follower_inboxes(pool).await?;
|
|
||||||
|
|
||||||
// Note: For an accept, since it was automatic, no one pushed a button,
|
|
||||||
// the community was the actor.
|
|
||||||
// But for delete, the creator is the actor, and does the signing
|
|
||||||
send_activity(client, &remove, mod_, inboxes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let group = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(group)?)?;
|
|
||||||
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![self.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(remove)?;
|
|
||||||
|
|
||||||
insert_activity(mod_.id, undo.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
let inboxes = self.get_follower_inboxes(pool).await?;
|
|
||||||
|
|
||||||
// Note: For an accept, since it was automatic, no one pushed a button,
|
|
||||||
// the community was the actor.
|
|
||||||
// But for remove , the creator is the actor, and does the signing
|
|
||||||
send_activity(client, &undo, mod_, inboxes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For a given community, returns the inboxes of all followers.
|
|
||||||
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError> {
|
|
||||||
let id = self.id;
|
|
||||||
|
|
||||||
let inboxes = blocking(pool, move |conn| {
|
|
||||||
CommunityFollowerView::for_community(conn, id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let inboxes = inboxes
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| get_shared_inbox(&c.user_actor_id))
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.unique()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(inboxes)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_follow(
|
|
||||||
&self,
|
|
||||||
_follow_actor_id: &str,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_unfollow(
|
|
||||||
&self,
|
|
||||||
_follow_actor_id: &str,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for CommunityForm {
|
|
||||||
type ApubType = GroupExt;
|
|
||||||
|
|
||||||
/// Parse an ActivityPub group received from another instance into a Lemmy community.
|
|
||||||
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
|
|
||||||
let group_extensions: &GroupExtension = &group.ext_one;
|
|
||||||
let oprops = &group.inner.object_props;
|
|
||||||
let aprops = &group.ext_two;
|
|
||||||
let public_key: &PublicKey = &group.ext_three.public_key;
|
|
||||||
|
|
||||||
let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
|
|
||||||
let creator_uri = creator_and_moderator_uris.next().unwrap();
|
|
||||||
|
|
||||||
let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
|
|
||||||
|
|
||||||
Ok(CommunityForm {
|
|
||||||
name: oprops.get_name_xsd_string().unwrap().to_string(),
|
|
||||||
title: aprops.get_preferred_username().unwrap().to_string(),
|
title: aprops.get_preferred_username().unwrap().to_string(),
|
||||||
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
|
description: oprops.get_summary_xsd_string().map(|s| s.to_string()),
|
||||||
// -> same for post.content etc
|
category_id: -1,
|
||||||
description: oprops.get_content_xsd_string().map(|s| s.to_string()),
|
creator_id: -1, //community.object_props.get_attributed_to_xsd_any_uri()
|
||||||
category_id: group_extensions.category.identifier.parse::<i32>()?,
|
removed: false,
|
||||||
creator_id: creator.id,
|
|
||||||
removed: None,
|
|
||||||
published: oprops
|
published: oprops
|
||||||
.get_published()
|
.get_published()
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.naive_local()
|
||||||
|
.to_owned(),
|
||||||
updated: oprops
|
updated: oprops
|
||||||
.get_updated()
|
.get_updated()
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||||
deleted: None,
|
deleted: false,
|
||||||
nsfw: group_extensions.sensitive,
|
nsfw: false,
|
||||||
actor_id: oprops.get_id().unwrap().to_string(),
|
creator_name: "".to_string(),
|
||||||
local: false,
|
creator_avatar: None,
|
||||||
private_key: None,
|
category_name: "".to_string(),
|
||||||
public_key: Some(public_key.to_owned().public_key_pem),
|
number_of_subscribers: *followers
|
||||||
last_refreshed_at: Some(naive_now()),
|
.collection_props
|
||||||
|
.get_total_items()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref() as i64,
|
||||||
|
number_of_posts: *outbox.collection_props.get_total_items().unwrap().as_ref() as i64,
|
||||||
|
number_of_comments: -1,
|
||||||
|
hot_rank: -1,
|
||||||
|
user_id: None,
|
||||||
|
subscribed: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the community json over HTTP.
|
|
||||||
pub async fn get_apub_community_http(
|
pub async fn get_apub_community_http(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityQuery>,
|
||||||
db: DbPoolParam,
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
let community = blocking(&db, move |conn| {
|
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||||
Community::read_from_name(conn, &info.community_name)
|
let c = community.as_group()?;
|
||||||
})
|
Ok(create_apub_response(&c))
|
||||||
.await??;
|
|
||||||
|
|
||||||
if !community.deleted {
|
|
||||||
let apub = community.to_apub(&db).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(
|
pub async fn get_apub_community_followers(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityQuery>,
|
||||||
db: DbPoolParam,
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
let community = blocking(&db, move |conn| {
|
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||||
Community::read_from_name(&conn, &info.community_name)
|
let base_url = make_apub_endpoint(EndpointType::Community, &community.name);
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let community_id = community.id;
|
let connection = establish_unpooled_connection();
|
||||||
let community_followers = blocking(&db, move |conn| {
|
//As we are an object, we validated that the community id was valid
|
||||||
CommunityFollowerView::for_community(&conn, community_id)
|
let community_followers =
|
||||||
})
|
CommunityFollowerView::for_community(&connection, community.id).unwrap();
|
||||||
.await??;
|
|
||||||
|
|
||||||
let mut collection = UnorderedCollection::default();
|
let mut collection = UnorderedCollection::default();
|
||||||
let oprops: &mut ObjectProperties = collection.as_mut();
|
let oprops: &mut ObjectProperties = collection.as_mut();
|
||||||
oprops
|
oprops
|
||||||
.set_context_xsd_any_uri(context())?
|
.set_context_xsd_any_uri(context())?
|
||||||
.set_id(community.actor_id)?;
|
.set_id(base_url)?;
|
||||||
collection
|
collection
|
||||||
.collection_props
|
.collection_props
|
||||||
.set_total_items(community_followers.len() as u64)?;
|
.set_total_items(community_followers.len() as u64)?;
|
||||||
Ok(create_apub_response(&collection))
|
Ok(create_apub_response(&collection))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Community {
|
pub async fn get_apub_community_outbox(
|
||||||
pub async fn do_announce<A>(
|
info: Path<CommunityQuery>,
|
||||||
activity: A,
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
community: &Community,
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
sender: &dyn ActorType,
|
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
|
||||||
client: &Client,
|
let base_url = make_apub_endpoint(EndpointType::Community, &community.name);
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<HttpResponse, LemmyError>
|
|
||||||
where
|
|
||||||
A: Activity + Base + Serialize + Debug,
|
|
||||||
{
|
|
||||||
let mut announce = Announce::default();
|
|
||||||
populate_object_props(
|
|
||||||
&mut announce.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
|
|
||||||
)?;
|
|
||||||
announce
|
|
||||||
.announce_props
|
|
||||||
.set_actor_xsd_any_uri(community.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
|
|
||||||
|
|
||||||
insert_activity(community.creator_id, announce.clone(), true, pool).await?;
|
let connection = establish_unpooled_connection();
|
||||||
|
//As we are an object, we validated that the community id was valid
|
||||||
|
let community_posts: Vec<Post> = Post::list_for_community(&connection, community.id)?;
|
||||||
|
|
||||||
// dont send to the instance where the activity originally came from, because that would result
|
let mut collection = OrderedCollection::default();
|
||||||
// in a database error (same data inserted twice)
|
let oprops: &mut ObjectProperties = collection.as_mut();
|
||||||
let mut to = community.get_follower_inboxes(pool).await?;
|
oprops
|
||||||
|
.set_context_xsd_any_uri(context())?
|
||||||
|
.set_id(base_url)?;
|
||||||
|
collection
|
||||||
|
.collection_props
|
||||||
|
.set_many_items_base_boxes(
|
||||||
|
community_posts
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.as_page().unwrap())
|
||||||
|
.collect(),
|
||||||
|
)?
|
||||||
|
.set_total_items(community_posts.len() as u64)?;
|
||||||
|
|
||||||
// this seems to be the "easiest" stable alternative for remove_item()
|
Ok(create_apub_response(&collection))
|
||||||
to.retain(|x| *x != sender.get_shared_inbox_url());
|
|
||||||
|
|
||||||
send_activity(client, &announce, community, to).await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,136 +0,0 @@
|
||||||
use crate::{
|
|
||||||
apub::{
|
|
||||||
extensions::signatures::verify,
|
|
||||||
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
|
|
||||||
ActorType,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
db::{
|
|
||||||
activity::insert_activity,
|
|
||||||
community::{Community, CommunityFollower, CommunityFollowerForm},
|
|
||||||
user::User_,
|
|
||||||
Followable,
|
|
||||||
},
|
|
||||||
routes::{ChatServerParam, DbPoolParam},
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::activity::Undo;
|
|
||||||
use activitystreams_new::activity::Follow;
|
|
||||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
|
||||||
use log::debug;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub enum CommunityAcceptedObjects {
|
|
||||||
Follow(Follow),
|
|
||||||
Undo(Undo),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommunityAcceptedObjects {
|
|
||||||
fn follow(&self) -> Result<Follow, LemmyError> {
|
|
||||||
match self {
|
|
||||||
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
|
|
||||||
CommunityAcceptedObjects::Undo(u) => Ok(
|
|
||||||
u.undo_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Follow>()?,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for all incoming activities to community inboxes.
|
|
||||||
pub async fn community_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
input: web::Json<CommunityAcceptedObjects>,
|
|
||||||
path: web::Path<String>,
|
|
||||||
db: DbPoolParam,
|
|
||||||
client: web::Data<Client>,
|
|
||||||
_chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let input = input.into_inner();
|
|
||||||
|
|
||||||
let path = path.into_inner();
|
|
||||||
let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;
|
|
||||||
|
|
||||||
if !community.local {
|
|
||||||
return Err(
|
|
||||||
format_err!(
|
|
||||||
"Received activity is addressed to remote community {}",
|
|
||||||
&community.actor_id
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
debug!(
|
|
||||||
"Community {} received activity {:?}",
|
|
||||||
&community.name, &input
|
|
||||||
);
|
|
||||||
let follow = input.follow()?;
|
|
||||||
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
|
|
||||||
let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
|
|
||||||
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &client, &db).await?;
|
|
||||||
|
|
||||||
verify(&request, &user)?;
|
|
||||||
|
|
||||||
match input {
|
|
||||||
CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
|
|
||||||
CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a follow request from a remote user, adding it to the local database and returning an
|
|
||||||
/// Accept activity.
|
|
||||||
async fn handle_follow(
|
|
||||||
follow: Follow,
|
|
||||||
user: User_,
|
|
||||||
community: Community,
|
|
||||||
client: &Client,
|
|
||||||
db: DbPoolParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
insert_activity(user.id, follow.clone(), false, &db).await?;
|
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
user_id: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail if they're already a follower, but ignore the error.
|
|
||||||
blocking(&db, move |conn| {
|
|
||||||
CommunityFollower::follow(&conn, &community_follower_form).ok()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
community.send_accept_follow(&follow, &client, &db).await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_undo_follow(
|
|
||||||
undo: Undo,
|
|
||||||
user: User_,
|
|
||||||
community: Community,
|
|
||||||
db: DbPoolParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
insert_activity(user.id, undo, false, &db).await?;
|
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
user_id: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail if they aren't a follower, but ignore the error.
|
|
||||||
blocking(&db, move |conn| {
|
|
||||||
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
use crate::{
|
|
||||||
db::{category::Category, Crud},
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{ext::Extension, Actor};
|
|
||||||
use diesel::PgConnection;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct GroupExtension {
|
|
||||||
pub category: GroupCategory,
|
|
||||||
pub sensitive: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct GroupCategory {
|
|
||||||
// Using a string because that's how Peertube does it.
|
|
||||||
pub identifier: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GroupExtension {
|
|
||||||
pub fn new(
|
|
||||||
conn: &PgConnection,
|
|
||||||
category_id: i32,
|
|
||||||
sensitive: bool,
|
|
||||||
) -> Result<GroupExtension, LemmyError> {
|
|
||||||
let category = Category::read(conn, category_id)?;
|
|
||||||
let group_category = GroupCategory {
|
|
||||||
identifier: category_id.to_string(),
|
|
||||||
name: category.name,
|
|
||||||
};
|
|
||||||
Ok(GroupExtension {
|
|
||||||
category: group_category,
|
|
||||||
sensitive,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Extension<T> for GroupExtension where T: Actor {}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod group_extensions;
|
|
||||||
pub mod page_extension;
|
|
||||||
pub mod signatures;
|
|
|
@ -1,11 +0,0 @@
|
||||||
use activitystreams::{ext::Extension, Base};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PageExtension {
|
|
||||||
pub comments_enabled: bool,
|
|
||||||
pub sensitive: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Extension<T> for PageExtension where T: Base {}
|
|
|
@ -1,119 +0,0 @@
|
||||||
use crate::{apub::ActorType, LemmyError};
|
|
||||||
use activitystreams::ext::Extension;
|
|
||||||
use actix_web::{client::ClientRequest, HttpRequest};
|
|
||||||
use http_signature_normalization_actix::{
|
|
||||||
digest::{DigestClient, SignExt},
|
|
||||||
Config,
|
|
||||||
};
|
|
||||||
use log::debug;
|
|
||||||
use openssl::{
|
|
||||||
hash::MessageDigest,
|
|
||||||
pkey::PKey,
|
|
||||||
rsa::Rsa,
|
|
||||||
sign::{Signer, Verifier},
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref HTTP_SIG_CONFIG: Config = Config::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Keypair {
|
|
||||||
pub private_key: String,
|
|
||||||
pub public_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
|
|
||||||
pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
|
|
||||||
let rsa = Rsa::generate(2048)?;
|
|
||||||
let pkey = PKey::from_rsa(rsa)?;
|
|
||||||
let public_key = pkey.public_key_to_pem()?;
|
|
||||||
let private_key = pkey.private_key_to_pem_pkcs8()?;
|
|
||||||
Ok(Keypair {
|
|
||||||
private_key: String::from_utf8(private_key)?,
|
|
||||||
public_key: String::from_utf8(public_key)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signs request headers with the given keypair.
|
|
||||||
pub async fn sign(
|
|
||||||
request: ClientRequest,
|
|
||||||
actor: &dyn ActorType,
|
|
||||||
activity: String,
|
|
||||||
) -> Result<DigestClient<String>, LemmyError> {
|
|
||||||
let signing_key_id = format!("{}#main-key", actor.actor_id());
|
|
||||||
let private_key = actor.private_key();
|
|
||||||
|
|
||||||
let digest_client = request
|
|
||||||
.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).unwrap();
|
|
||||||
signer.update(signing_string.as_bytes()).unwrap();
|
|
||||||
|
|
||||||
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError>
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(digest_client)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
|
|
||||||
let verified = HTTP_SIG_CONFIG
|
|
||||||
.begin_verify(
|
|
||||||
request.method(),
|
|
||||||
request.uri().path_and_query(),
|
|
||||||
request.headers().clone(),
|
|
||||||
)?
|
|
||||||
.verify(|signature, signing_string| -> Result<bool, LemmyError> {
|
|
||||||
debug!(
|
|
||||||
"Verifying with key {}, message {}",
|
|
||||||
&actor.public_key(),
|
|
||||||
&signing_string
|
|
||||||
);
|
|
||||||
let public_key = PKey::public_key_from_pem(actor.public_key().as_bytes())?;
|
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap();
|
|
||||||
verifier.update(&signing_string.as_bytes()).unwrap();
|
|
||||||
Ok(verifier.verify(&base64::decode(signature)?)?)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if verified {
|
|
||||||
debug!("verified signature for {}", &request.uri());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(format_err!("Invalid signature on request: {}", &request.uri()).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following is taken from here:
|
|
||||||
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PublicKey {
|
|
||||||
pub id: String,
|
|
||||||
pub owner: String,
|
|
||||||
pub public_key_pem: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PublicKeyExtension {
|
|
||||||
pub public_key: PublicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PublicKey {
|
|
||||||
pub fn to_ext(&self) -> PublicKeyExtension {
|
|
||||||
PublicKeyExtension {
|
|
||||||
public_key: self.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Extension<T> for PublicKeyExtension where T: activitystreams::Actor {}
|
|
|
@ -1,423 +0,0 @@
|
||||||
use activitystreams::object::Note;
|
|
||||||
use actix_web::client::Client;
|
|
||||||
use diesel::{result::Error::NotFound, PgConnection};
|
|
||||||
use log::debug;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{fmt::Debug, time::Duration};
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
api::site::SearchResponse,
|
|
||||||
blocking,
|
|
||||||
db::{
|
|
||||||
comment::{Comment, CommentForm},
|
|
||||||
comment_view::CommentView,
|
|
||||||
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
|
|
||||||
community_view::CommunityView,
|
|
||||||
post::{Post, PostForm},
|
|
||||||
post_view::PostView,
|
|
||||||
user::{UserForm, User_},
|
|
||||||
Crud,
|
|
||||||
Joinable,
|
|
||||||
SearchType,
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
request::{retry, RecvError},
|
|
||||||
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
apub::{
|
|
||||||
get_apub_protocol_string,
|
|
||||||
is_apub_id_valid,
|
|
||||||
FromApub,
|
|
||||||
GroupExt,
|
|
||||||
PageExt,
|
|
||||||
PersonExt,
|
|
||||||
APUB_JSON_CONTENT_TYPE,
|
|
||||||
},
|
|
||||||
db::user_view::UserView,
|
|
||||||
};
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
|
|
||||||
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
|
|
||||||
|
|
||||||
// Fetch nodeinfo metadata from a remote instance.
|
|
||||||
async fn _fetch_node_info(client: &Client, domain: &str) -> Result<NodeInfo, LemmyError> {
|
|
||||||
let well_known_uri = Url::parse(&format!(
|
|
||||||
"{}://{}/.well-known/nodeinfo",
|
|
||||||
get_apub_protocol_string(),
|
|
||||||
domain
|
|
||||||
))?;
|
|
||||||
|
|
||||||
let well_known = fetch_remote_object::<NodeInfoWellKnown>(client, &well_known_uri).await?;
|
|
||||||
let nodeinfo = fetch_remote_object::<NodeInfo>(client, &well_known.links.href).await?;
|
|
||||||
|
|
||||||
Ok(nodeinfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
|
|
||||||
/// timeouts etc.
|
|
||||||
pub async fn fetch_remote_object<Response>(
|
|
||||||
client: &Client,
|
|
||||||
url: &Url,
|
|
||||||
) -> Result<Response, LemmyError>
|
|
||||||
where
|
|
||||||
Response: for<'de> Deserialize<'de>,
|
|
||||||
{
|
|
||||||
if !is_apub_id_valid(&url) {
|
|
||||||
return Err(format_err!("Activitypub uri invalid or blocked: {}", url).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(60);
|
|
||||||
|
|
||||||
let json = retry(|| {
|
|
||||||
client
|
|
||||||
.get(url.as_str())
|
|
||||||
.header("Accept", APUB_JSON_CONTENT_TYPE)
|
|
||||||
.timeout(timeout)
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
debug!("Receive error, {}", e);
|
|
||||||
RecvError(e.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[derive(serde::Deserialize, Debug)]
|
|
||||||
pub enum SearchAcceptedObjects {
|
|
||||||
Person(Box<PersonExt>),
|
|
||||||
Group(Box<GroupExt>),
|
|
||||||
Page(Box<PageExt>),
|
|
||||||
Comment(Box<Note>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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:8540/c/main, or !main@lemmy_alpha:8540
|
|
||||||
/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
|
|
||||||
/// http://lemmy_alpha:8540/post/3
|
|
||||||
/// http://lemmy_alpha:8540/comment/2
|
|
||||||
pub async fn search_by_apub_id(
|
|
||||||
query: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<SearchResponse, LemmyError> {
|
|
||||||
// Parse the shorthand query url
|
|
||||||
let query_url = if query.contains('@') {
|
|
||||||
debug!("{}", query);
|
|
||||||
let split = query.split('@').collect::<Vec<&str>>();
|
|
||||||
|
|
||||||
// User 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(format_err!("Invalid search query: {}", query).into());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(format_err!("Invalid search query: {}", query).into());
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
|
|
||||||
Url::parse(&url)?
|
|
||||||
} else {
|
|
||||||
Url::parse(&query)?
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut response = SearchResponse {
|
|
||||||
type_: SearchType::All.to_string(),
|
|
||||||
comments: vec![],
|
|
||||||
posts: vec![],
|
|
||||||
communities: vec![],
|
|
||||||
users: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
|
|
||||||
SearchAcceptedObjects::Person(p) => {
|
|
||||||
let user_uri = p.inner.object_props.get_id().unwrap().to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
|
||||||
|
|
||||||
response.users = vec![blocking(pool, move |conn| UserView::read(conn, user.id)).await??];
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
SearchAcceptedObjects::Group(g) => {
|
|
||||||
let community_uri = g.inner.object_props.get_id().unwrap().to_string();
|
|
||||||
|
|
||||||
let community =
|
|
||||||
get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
|
|
||||||
|
|
||||||
// TODO Maybe at some point in the future, fetch all the history of a community
|
|
||||||
// fetch_community_outbox(&c, conn)?;
|
|
||||||
response.communities = vec![
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
CommunityView::read(conn, community.id, None)
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
];
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
SearchAcceptedObjects::Page(p) => {
|
|
||||||
let post_form = PostForm::from_apub(&p, client, pool).await?;
|
|
||||||
|
|
||||||
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
|
|
||||||
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
SearchAcceptedObjects::Comment(c) => {
|
|
||||||
let post_url = c
|
|
||||||
.object_props
|
|
||||||
.get_many_in_reply_to_xsd_any_uris()
|
|
||||||
.unwrap()
|
|
||||||
.next()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// TODO: also fetch parent comments if any
|
|
||||||
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
|
|
||||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
|
||||||
let comment_form = CommentForm::from_apub(&c, client, pool).await?;
|
|
||||||
|
|
||||||
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
|
|
||||||
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
|
|
||||||
response.comments =
|
|
||||||
vec![blocking(pool, move |conn| CommentView::read(conn, c.id, None)).await??];
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
|
|
||||||
pub async fn get_or_fetch_and_upsert_remote_user(
|
|
||||||
apub_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<User_, LemmyError> {
|
|
||||||
let apub_id_owned = apub_id.to_owned();
|
|
||||||
let user = blocking(pool, move |conn| {
|
|
||||||
User_::read_from_actor_id(conn, &apub_id_owned)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match user {
|
|
||||||
// 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 user: {}", apub_id);
|
|
||||||
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
|
|
||||||
|
|
||||||
let mut uf = UserForm::from_apub(&person, client, pool).await?;
|
|
||||||
uf.last_refreshed_at = Some(naive_now());
|
|
||||||
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
Ok(u) => Ok(u),
|
|
||||||
Err(NotFound {}) => {
|
|
||||||
debug!("Fetching and creating remote user: {}", apub_id);
|
|
||||||
let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
|
|
||||||
|
|
||||||
let uf = UserForm::from_apub(&person, client, pool).await?;
|
|
||||||
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 always.
|
|
||||||
///
|
|
||||||
/// 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 {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
let update_interval = chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS);
|
|
||||||
last_refreshed.lt(&(naive_now() - update_interval))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
|
|
||||||
pub async fn get_or_fetch_and_upsert_remote_community(
|
|
||||||
apub_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Community, LemmyError> {
|
|
||||||
let apub_id_owned = apub_id.to_owned();
|
|
||||||
let community = blocking(pool, move |conn| {
|
|
||||||
Community::read_from_actor_id(conn, &apub_id_owned)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match community {
|
|
||||||
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
|
|
||||||
debug!("Fetching and updating from remote community: {}", apub_id);
|
|
||||||
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
|
|
||||||
|
|
||||||
let mut cf = CommunityForm::from_apub(&group, client, pool).await?;
|
|
||||||
cf.last_refreshed_at = Some(naive_now());
|
|
||||||
let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
|
|
||||||
|
|
||||||
Ok(community)
|
|
||||||
}
|
|
||||||
Ok(c) => Ok(c),
|
|
||||||
Err(NotFound {}) => {
|
|
||||||
debug!("Fetching and creating remote community: {}", apub_id);
|
|
||||||
let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
|
|
||||||
|
|
||||||
let cf = CommunityForm::from_apub(&group, client, pool).await?;
|
|
||||||
let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
|
|
||||||
|
|
||||||
// Also add the community moderators too
|
|
||||||
let creator_and_moderator_uris = group
|
|
||||||
.inner
|
|
||||||
.object_props
|
|
||||||
.get_many_attributed_to_xsd_any_uris()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut creator_and_moderators = Vec::new();
|
|
||||||
|
|
||||||
for uri in creator_and_moderator_uris {
|
|
||||||
let c_or_m = get_or_fetch_and_upsert_remote_user(uri.as_str(), client, pool).await?;
|
|
||||||
|
|
||||||
creator_and_moderators.push(c_or_m);
|
|
||||||
}
|
|
||||||
|
|
||||||
let community_id = community.id;
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
for mod_ in creator_and_moderators {
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
|
||||||
community_id,
|
|
||||||
user_id: mod_.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
CommunityModerator::join(conn, &community_moderator_form)?;
|
|
||||||
}
|
|
||||||
Ok(()) as Result<(), LemmyError>
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
Ok(community)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyError> {
|
|
||||||
let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
|
|
||||||
match existing {
|
|
||||||
Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
|
|
||||||
Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_or_fetch_and_insert_remote_post(
|
|
||||||
post_ap_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Post, LemmyError> {
|
|
||||||
let post_ap_id_owned = post_ap_id.to_owned();
|
|
||||||
let post = blocking(pool, move |conn| {
|
|
||||||
Post::read_from_apub_id(conn, &post_ap_id_owned)
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match post {
|
|
||||||
Ok(p) => Ok(p),
|
|
||||||
Err(NotFound {}) => {
|
|
||||||
debug!("Fetching and creating remote post: {}", post_ap_id);
|
|
||||||
let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
|
|
||||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
|
||||||
|
|
||||||
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
|
|
||||||
|
|
||||||
Ok(post)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, LemmyError> {
|
|
||||||
let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
|
|
||||||
match existing {
|
|
||||||
Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
|
|
||||||
Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_or_fetch_and_insert_remote_comment(
|
|
||||||
comment_ap_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Comment, LemmyError> {
|
|
||||||
let comment_ap_id_owned = comment_ap_id.to_owned();
|
|
||||||
let comment = blocking(pool, move |conn| {
|
|
||||||
Comment::read_from_apub_id(conn, &comment_ap_id_owned)
|
|
||||||
})
|
|
||||||
.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::<Note>(client, &Url::parse(comment_ap_id)?).await?;
|
|
||||||
let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
|
|
||||||
|
|
||||||
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
|
|
||||||
|
|
||||||
Ok(comment)
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO It should not be fetching data from a community outbox.
|
|
||||||
// All posts, comments, comment likes, etc should be posts to our community_inbox
|
|
||||||
// The only data we should be periodically fetching (if it hasn't been fetched in the last day
|
|
||||||
// maybe), is community and user actors
|
|
||||||
// and user actors
|
|
||||||
// Fetch all posts in the outbox of the given user, and insert them into the database.
|
|
||||||
// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, LemmyError> {
|
|
||||||
// let outbox_url = Url::parse(&community.get_outbox_url())?;
|
|
||||||
// let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
|
|
||||||
// let items = outbox.collection_props.get_many_items_base_boxes();
|
|
||||||
|
|
||||||
// Ok(
|
|
||||||
// items
|
|
||||||
// .unwrap()
|
|
||||||
// .map(|obox: &BaseBox| -> Result<PostForm, LemmyError> {
|
|
||||||
// let page = obox.clone().to_concrete::<Page>()?;
|
|
||||||
// PostForm::from_page(&page, conn)
|
|
||||||
// })
|
|
||||||
// .map(|pf| upsert_post(&pf?, conn))
|
|
||||||
// .collect::<Result<Vec<Post>, LemmyError>>()?,
|
|
||||||
// )
|
|
||||||
// }
|
|
|
@ -1,88 +1,47 @@
|
||||||
pub mod activities;
|
|
||||||
pub mod comment;
|
|
||||||
pub mod community;
|
pub mod community;
|
||||||
pub mod community_inbox;
|
|
||||||
pub mod extensions;
|
|
||||||
pub mod fetcher;
|
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod private_message;
|
pub mod puller;
|
||||||
pub mod shared_inbox;
|
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_inbox;
|
use crate::Settings;
|
||||||
|
use openssl::{pkey::PKey, rsa::Rsa};
|
||||||
|
|
||||||
use crate::{
|
use activitystreams::actor::{properties::ApActorProperties, Group};
|
||||||
apub::extensions::{
|
use activitystreams::ext::Ext;
|
||||||
group_extensions::GroupExtension,
|
use actix_web::body::Body;
|
||||||
page_extension::PageExtension,
|
use actix_web::HttpResponse;
|
||||||
signatures::{PublicKey, PublicKeyExtension},
|
|
||||||
},
|
|
||||||
convert_datetime,
|
|
||||||
db::user::User_,
|
|
||||||
request::{retry, RecvError},
|
|
||||||
routes::webfinger::WebFingerResponse,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
MentionData,
|
|
||||||
Settings,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
actor::{properties::ApActorProperties, Group, Person},
|
|
||||||
object::Page,
|
|
||||||
};
|
|
||||||
use activitystreams_ext::{Ext1, Ext2, Ext3};
|
|
||||||
use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
|
|
||||||
use actix_web::{body::Body, client::Client, HttpResponse};
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use log::debug;
|
|
||||||
use serde::Serialize;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
type GroupExt = Ext3<Group, GroupExtension, ApActorProperties, PublicKeyExtension>;
|
type GroupExt = Ext<Group, ApActorProperties>;
|
||||||
type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>;
|
|
||||||
type PageExt = Ext1<Page, PageExtension>;
|
|
||||||
|
|
||||||
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
|
fn create_apub_response<T>(json: &T) -> HttpResponse<Body>
|
||||||
|
where
|
||||||
|
T: serde::ser::Serialize,
|
||||||
|
{
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.json(json)
|
||||||
|
}
|
||||||
|
|
||||||
pub enum EndpointType {
|
pub enum EndpointType {
|
||||||
Community,
|
Community,
|
||||||
User,
|
User,
|
||||||
Post,
|
Post,
|
||||||
Comment,
|
Comment,
|
||||||
PrivateMessage,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
|
// TODO: we will probably need to change apub endpoint urls so that html and activity+json content
|
||||||
/// headers.
|
// types are handled at the same endpoint, so that you can copy the url into mastodon search
|
||||||
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
|
// and have it fetch the object.
|
||||||
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)
|
|
||||||
.json(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates the ActivityPub ID for a given object type and ID.
|
|
||||||
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
|
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
|
||||||
let point = match endpoint_type {
|
let point = match endpoint_type {
|
||||||
EndpointType::Community => "c",
|
EndpointType::Community => "community",
|
||||||
EndpointType::User => "u",
|
EndpointType::User => "user",
|
||||||
EndpointType::Post => "post",
|
EndpointType::Post => "post",
|
||||||
EndpointType::Comment => "comment",
|
EndpointType::Comment => "comment",
|
||||||
EndpointType::PrivateMessage => "private_message",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Url::parse(&format!(
|
Url::parse(&format!(
|
||||||
"{}://{}/{}/{}",
|
"{}://{}/federation/{}/{}",
|
||||||
get_apub_protocol_string(),
|
get_apub_protocol_string(),
|
||||||
Settings::get().hostname,
|
Settings::get().hostname,
|
||||||
point,
|
point,
|
||||||
|
@ -99,276 +58,53 @@ pub fn get_apub_protocol_string() -> &'static str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
|
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||||
fn is_apub_id_valid(apub_id: &Url) -> bool {
|
let rsa = Rsa::generate(2048).expect("sign::gen_keypair: key generation error");
|
||||||
debug!("Checking {}", apub_id);
|
let pkey = PKey::from_rsa(rsa).expect("sign::gen_keypair: parsing error");
|
||||||
if apub_id.scheme() != get_apub_protocol_string() {
|
(
|
||||||
debug!("invalid scheme: {:?}", apub_id.scheme());
|
pkey
|
||||||
return false;
|
.public_key_to_pem()
|
||||||
}
|
.expect("sign::gen_keypair: public key encoding error"),
|
||||||
|
pkey
|
||||||
let allowed_instances: Vec<String> = Settings::get()
|
.private_key_to_pem_pkcs8()
|
||||||
.federation
|
.expect("sign::gen_keypair: private key encoding error"),
|
||||||
.allowed_instances
|
|
||||||
.split(',')
|
|
||||||
.map(|d| d.to_string())
|
|
||||||
.collect();
|
|
||||||
match apub_id.domain() {
|
|
||||||
Some(d) => {
|
|
||||||
let contains = allowed_instances.contains(&d.to_owned());
|
|
||||||
|
|
||||||
if !contains {
|
|
||||||
debug!("{} not in {:?}", d, allowed_instances);
|
|
||||||
}
|
|
||||||
|
|
||||||
contains
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
debug!("missing domain");
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
pub trait ToApub {
|
|
||||||
type Response;
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updated is actually the deletion time
|
|
||||||
fn create_tombstone(
|
|
||||||
deleted: bool,
|
|
||||||
object_id: &str,
|
|
||||||
updated: Option<NaiveDateTime>,
|
|
||||||
former_type: String,
|
|
||||||
) -> Result<Tombstone, LemmyError> {
|
|
||||||
if deleted {
|
|
||||||
if let Some(updated) = updated {
|
|
||||||
let mut tombstone = Tombstone::new();
|
|
||||||
tombstone.set_id(object_id.parse()?);
|
|
||||||
tombstone.set_former_type(former_type);
|
|
||||||
tombstone.set_deleted(convert_datetime(updated).into());
|
|
||||||
Ok(tombstone)
|
|
||||||
} else {
|
|
||||||
Err(format_err!("Cant convert to tombstone because updated time was None.").into())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
pub trait FromApub {
|
|
||||||
type ApubType;
|
|
||||||
async fn from_apub(
|
|
||||||
apub: &Self::ApubType,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<Self, LemmyError>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
pub trait ApubObjectType {
|
|
||||||
async fn send_create(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_update(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
pub trait ApubLikeableType {
|
|
||||||
async fn send_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_dislike(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_undo_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_shared_inbox(actor_id: &str) -> String {
|
|
||||||
let url = Url::parse(actor_id).unwrap();
|
|
||||||
format!(
|
|
||||||
"{}://{}{}/inbox",
|
|
||||||
&url.scheme(),
|
|
||||||
&url.host_str().unwrap(),
|
|
||||||
if let Some(port) = url.port() {
|
|
||||||
format!(":{}", port)
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
pub fn gen_keypair_str() -> (String, String) {
|
||||||
pub trait ActorType {
|
let (public_key, private_key) = gen_keypair();
|
||||||
fn actor_id(&self) -> String;
|
(vec_bytes_to_str(public_key), vec_bytes_to_str(private_key))
|
||||||
|
}
|
||||||
|
|
||||||
fn public_key(&self) -> String;
|
fn vec_bytes_to_str(bytes: Vec<u8>) -> String {
|
||||||
fn private_key(&self) -> String;
|
String::from_utf8_lossy(&bytes).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
// These two have default impls, since currently a community can't follow anything,
|
/// If community is on local instance, don't include the @instance part. This is only for displaying
|
||||||
// and a user can't be followed (yet)
|
/// to the user and should never be used otherwise.
|
||||||
#[allow(unused_variables)]
|
pub fn format_community_name(name: &str, instance: &str) -> String {
|
||||||
async fn send_follow(
|
if instance == Settings::get().hostname {
|
||||||
&self,
|
format!("!{}", name)
|
||||||
follow_actor_id: &str,
|
} else {
|
||||||
client: &Client,
|
format!("!{}@{}", name, instance)
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_unfollow(
|
|
||||||
&self,
|
|
||||||
follow_actor_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
async fn send_accept_follow(
|
|
||||||
&self,
|
|
||||||
follow: &Follow,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>;
|
|
||||||
|
|
||||||
/// For a given community, returns the inboxes of all followers.
|
|
||||||
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
|
|
||||||
|
|
||||||
// TODO move these to the db rows
|
|
||||||
fn get_inbox_url(&self) -> String {
|
|
||||||
format!("{}/inbox", &self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_shared_inbox_url(&self) -> String {
|
|
||||||
get_shared_inbox(&self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_outbox_url(&self) -> String {
|
|
||||||
format!("{}/outbox", &self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_followers_url(&self) -> String {
|
|
||||||
format!("{}/followers", &self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_following_url(&self) -> String {
|
|
||||||
format!("{}/following", &self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_liked_url(&self) -> String {
|
|
||||||
format!("{}/liked", &self.actor_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_public_key_ext(&self) -> PublicKeyExtension {
|
|
||||||
PublicKey {
|
|
||||||
id: format!("{}#main-key", self.actor_id()),
|
|
||||||
owner: self.actor_id(),
|
|
||||||
public_key_pem: self.public_key(),
|
|
||||||
}
|
|
||||||
.to_ext()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_webfinger_url(
|
pub fn get_following_instances() -> Vec<&'static str> {
|
||||||
mention: &MentionData,
|
Settings::get()
|
||||||
client: &Client,
|
.federation
|
||||||
) -> Result<String, LemmyError> {
|
.followed_instances
|
||||||
let fetch_url = format!(
|
.split(',')
|
||||||
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
|
.collect()
|
||||||
get_apub_protocol_string(),
|
}
|
||||||
mention.domain,
|
|
||||||
mention.name,
|
/// Returns a tuple of (username, domain) from an identifier like "main@dev.lemmy.ml"
|
||||||
mention.domain
|
fn split_identifier(identifier: &str) -> (String, String) {
|
||||||
);
|
let x: Vec<&str> = identifier.split('@').collect();
|
||||||
debug!("Fetching webfinger url: {}", &fetch_url);
|
(x[0].replace("!", ""), x[1].to_string())
|
||||||
|
}
|
||||||
let mut response = retry(|| client.get(&fetch_url).send()).await?;
|
|
||||||
|
fn get_remote_community_uri(identifier: &str) -> String {
|
||||||
let res: WebFingerResponse = response
|
let (name, domain) = split_identifier(identifier);
|
||||||
.json()
|
format!("http://{}/federation/c/{}", domain, name)
|
||||||
.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(|| format_err!("No application/activity+json link found."))?;
|
|
||||||
link
|
|
||||||
.href
|
|
||||||
.to_owned()
|
|
||||||
.ok_or_else(|| format_err!("No href found.").into())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,14 @@
|
||||||
use crate::{
|
use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType};
|
||||||
apub::{
|
use crate::db::post::Post;
|
||||||
activities::{populate_object_props, send_activity_to_community},
|
use crate::db::post_view::PostView;
|
||||||
create_apub_response,
|
use crate::{convert_datetime, naive_now};
|
||||||
create_apub_tombstone_response,
|
use activitystreams::{object::properties::ObjectProperties, object::Page};
|
||||||
create_tombstone,
|
use actix_web::body::Body;
|
||||||
extensions::page_extension::PageExtension,
|
use actix_web::web::Path;
|
||||||
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
|
use actix_web::{web, HttpResponse};
|
||||||
get_apub_protocol_string,
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
ActorType,
|
use diesel::PgConnection;
|
||||||
ApubLikeableType,
|
use failure::Error;
|
||||||
ApubObjectType,
|
|
||||||
FromApub,
|
|
||||||
PageExt,
|
|
||||||
ToApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
convert_datetime,
|
|
||||||
db::{
|
|
||||||
community::Community,
|
|
||||||
post::{Post, PostForm},
|
|
||||||
user::User_,
|
|
||||||
Crud,
|
|
||||||
},
|
|
||||||
routes::DbPoolParam,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
Settings,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
|
|
||||||
context,
|
|
||||||
object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page},
|
|
||||||
BaseBox,
|
|
||||||
};
|
|
||||||
use activitystreams_ext::Ext1;
|
|
||||||
use activitystreams_new::object::Tombstone;
|
|
||||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -43,579 +16,98 @@ pub struct PostQuery {
|
||||||
post_id: String,
|
post_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the post json over HTTP.
|
|
||||||
pub async fn get_apub_post(
|
pub async fn get_apub_post(
|
||||||
info: web::Path<PostQuery>,
|
info: Path<PostQuery>,
|
||||||
db: DbPoolParam,
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
let id = info.post_id.parse::<i32>()?;
|
let id = info.post_id.parse::<i32>()?;
|
||||||
let post = blocking(&db, move |conn| Post::read(conn, id)).await??;
|
let post = Post::read(&&db.get()?, id)?;
|
||||||
|
Ok(create_apub_response(&post.as_page()?))
|
||||||
if !post.deleted {
|
|
||||||
Ok(create_apub_response(&post.to_apub(&db).await?))
|
|
||||||
} else {
|
|
||||||
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl Post {
|
||||||
impl ToApub for Post {
|
pub fn as_page(&self) -> Result<Page, Error> {
|
||||||
type Response = PageExt;
|
let base_url = make_apub_endpoint(EndpointType::Post, &self.id.to_string());
|
||||||
|
|
||||||
// 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 = Page::default();
|
let mut page = Page::default();
|
||||||
let oprops: &mut ObjectProperties = page.as_mut();
|
let oprops: &mut ObjectProperties = page.as_mut();
|
||||||
|
|
||||||
let creator_id = self.creator_id;
|
|
||||||
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
oprops
|
oprops
|
||||||
// Not needed when the Post is embedded in a collection (like for community outbox)
|
// Not needed when the Post is embedded in a collection (like for community outbox)
|
||||||
// TODO: need to set proper context defining sensitive/commentsEnabled fields
|
//.set_context_xsd_any_uri(context())?
|
||||||
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
|
.set_id(base_url)?
|
||||||
.set_context_xsd_any_uri(context())?
|
.set_name_xsd_string(self.name.to_owned())?
|
||||||
.set_id(self.ap_id.to_owned())?
|
|
||||||
// Use summary field to be consistent with mastodon content warning.
|
|
||||||
// https://mastodon.xyz/@Louisa/103987265222901387.json
|
|
||||||
.set_summary_xsd_string(self.name.to_owned())?
|
|
||||||
.set_published(convert_datetime(self.published))?
|
.set_published(convert_datetime(self.published))?
|
||||||
.set_to_xsd_any_uri(community.actor_id)?
|
.set_attributed_to_xsd_any_uri(make_apub_endpoint(
|
||||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
EndpointType::User,
|
||||||
|
&self.creator_id.to_string(),
|
||||||
|
))?;
|
||||||
|
|
||||||
if let Some(body) = &self.body {
|
if let Some(body) = &self.body {
|
||||||
oprops.set_content_xsd_string(body.to_owned())?;
|
oprops.set_content_xsd_string(body.to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: hacky code because we get self.url == Some("")
|
// TODO: hacky code because we get self.url == Some("")
|
||||||
// https://github.com/LemmyNet/lemmy/issues/602
|
// https://github.com/dessalines/lemmy/issues/602
|
||||||
let url = self.url.as_ref().filter(|u| !u.is_empty());
|
let url = self.url.as_ref().filter(|u| !u.is_empty());
|
||||||
if let Some(u) = url {
|
if let Some(u) = url {
|
||||||
oprops.set_url_xsd_any_uri(u.to_owned())?;
|
oprops.set_url_xsd_any_uri(u.to_owned())?;
|
||||||
|
|
||||||
// Embeds
|
|
||||||
let mut page_preview = Page::new();
|
|
||||||
page_preview
|
|
||||||
.object_props
|
|
||||||
.set_url_xsd_any_uri(u.to_owned())?;
|
|
||||||
|
|
||||||
if let Some(embed_title) = &self.embed_title {
|
|
||||||
page_preview
|
|
||||||
.object_props
|
|
||||||
.set_name_xsd_string(embed_title.to_owned())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(embed_description) = &self.embed_description {
|
|
||||||
page_preview
|
|
||||||
.object_props
|
|
||||||
.set_summary_xsd_string(embed_description.to_owned())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(embed_html) = &self.embed_html {
|
|
||||||
page_preview
|
|
||||||
.object_props
|
|
||||||
.set_content_xsd_string(embed_html.to_owned())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
oprops.set_preview_base_box(page_preview)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(thumbnail_url) = &self.thumbnail_url {
|
|
||||||
let full_url = format!(
|
|
||||||
"{}://{}/pictshare/{}",
|
|
||||||
get_apub_protocol_string(),
|
|
||||||
Settings::get().hostname,
|
|
||||||
thumbnail_url
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut image = Image::new();
|
|
||||||
image.object_props.set_url_xsd_any_uri(full_url)?;
|
|
||||||
let any_image = AnyImage::from_concrete(image)?;
|
|
||||||
oprops.set_image_any_image(any_image)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(u) = self.updated {
|
if let Some(u) = self.updated {
|
||||||
oprops.set_updated(convert_datetime(u))?;
|
oprops.set_updated(convert_datetime(u))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext = PageExtension {
|
Ok(page)
|
||||||
comments_enabled: !self.locked,
|
|
||||||
sensitive: self.nsfw,
|
|
||||||
};
|
|
||||||
Ok(Ext1::new(page, ext))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
create_tombstone(
|
|
||||||
self.deleted,
|
|
||||||
&self.ap_id,
|
|
||||||
self.updated,
|
|
||||||
PageType.to_string(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
impl PostView {
|
||||||
impl FromApub for PostForm {
|
pub fn from_page(page: &Page) -> Result<PostView, Error> {
|
||||||
type ApubType = PageExt;
|
let oprops = &page.object_props;
|
||||||
|
Ok(PostView {
|
||||||
/// Parse an ActivityPub page received from another instance into a Lemmy post.
|
id: -1,
|
||||||
async fn from_apub(
|
name: oprops.get_name_xsd_string().unwrap().to_string(),
|
||||||
page: &PageExt,
|
url: oprops.get_url_xsd_any_uri().map(|u| u.to_string()),
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<PostForm, LemmyError> {
|
|
||||||
let ext = &page.ext_one;
|
|
||||||
let oprops = &page.inner.object_props;
|
|
||||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
|
|
||||||
|
|
||||||
let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let community =
|
|
||||||
get_or_fetch_and_upsert_remote_community(&community_actor_id, client, pool).await?;
|
|
||||||
|
|
||||||
let thumbnail_url = match oprops.get_image_any_image() {
|
|
||||||
Some(any_image) => any_image
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Image>()?
|
|
||||||
.object_props
|
|
||||||
.get_url_xsd_any_uri()
|
|
||||||
.map(|u| u.to_string()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = oprops.get_url_xsd_any_uri().map(|u| u.to_string());
|
|
||||||
let (embed_title, embed_description, embed_html) = match oprops.get_preview_base_box() {
|
|
||||||
Some(preview) => {
|
|
||||||
let preview_page = preview.to_owned().into_concrete::<Page>()?;
|
|
||||||
let name = preview_page
|
|
||||||
.object_props
|
|
||||||
.get_name_xsd_string()
|
|
||||||
.map(|n| n.to_string());
|
|
||||||
let summary = preview_page
|
|
||||||
.object_props
|
|
||||||
.get_summary_xsd_string()
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
let content = preview_page
|
|
||||||
.object_props
|
|
||||||
.get_content_xsd_string()
|
|
||||||
.map(|c| c.to_string());
|
|
||||||
(name, summary, content)
|
|
||||||
}
|
|
||||||
None => (None, None, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PostForm {
|
|
||||||
name: oprops.get_summary_xsd_string().unwrap().to_string(),
|
|
||||||
url,
|
|
||||||
body: oprops.get_content_xsd_string().map(|c| c.to_string()),
|
body: oprops.get_content_xsd_string().map(|c| c.to_string()),
|
||||||
creator_id: creator.id,
|
creator_id: -1,
|
||||||
community_id: community.id,
|
community_id: -1,
|
||||||
removed: None,
|
removed: false,
|
||||||
locked: Some(!ext.comments_enabled),
|
locked: false,
|
||||||
published: oprops
|
published: oprops
|
||||||
.get_published()
|
.get_published()
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.naive_local()
|
||||||
|
.to_owned(),
|
||||||
updated: oprops
|
updated: oprops
|
||||||
.get_updated()
|
.get_updated()
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
.map(|u| u.as_ref().to_owned().naive_local()),
|
||||||
deleted: None,
|
deleted: false,
|
||||||
nsfw: ext.sensitive,
|
nsfw: false,
|
||||||
stickied: None, // -> put it in "featured" collection of the community
|
stickied: false,
|
||||||
embed_title,
|
embed_title: None,
|
||||||
embed_description,
|
embed_description: None,
|
||||||
embed_html,
|
embed_html: None,
|
||||||
thumbnail_url,
|
thumbnail_url: None,
|
||||||
ap_id: oprops.get_id().unwrap().to_string(),
|
banned: false,
|
||||||
local: false,
|
banned_from_community: false,
|
||||||
|
creator_name: "".to_string(),
|
||||||
|
creator_avatar: None,
|
||||||
|
community_name: "".to_string(),
|
||||||
|
community_removed: false,
|
||||||
|
community_deleted: false,
|
||||||
|
community_nsfw: false,
|
||||||
|
number_of_comments: -1,
|
||||||
|
score: -1,
|
||||||
|
upvotes: -1,
|
||||||
|
downvotes: -1,
|
||||||
|
hot_rank: -1,
|
||||||
|
newest_activity_time: naive_now(),
|
||||||
|
user_id: None,
|
||||||
|
my_vote: None,
|
||||||
|
subscribed: None,
|
||||||
|
read: None,
|
||||||
|
saved: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut create = Create::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut create.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
create
|
|
||||||
.create_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
create,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send out information about an edited post, to the followers of the community.
|
|
||||||
async fn send_update(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut update = Update::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut update.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
update
|
|
||||||
.update_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
update,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
delete,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut delete = Delete::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut delete.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(delete)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
mod_,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
remove,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
mod_: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut remove = Remove::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut remove.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
remove
|
|
||||||
.remove_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(remove)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
mod_,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ApubLikeableType for Post {
|
|
||||||
async fn send_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut like = Like::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut like.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
like
|
|
||||||
.like_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
like,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_dislike(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut dislike = Dislike::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut dislike.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
dislike
|
|
||||||
.dislike_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
dislike,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_like(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let page = self.to_apub(pool).await?;
|
|
||||||
|
|
||||||
let community_id = self.community_id;
|
|
||||||
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
|
|
||||||
|
|
||||||
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let mut like = Like::new();
|
|
||||||
populate_object_props(
|
|
||||||
&mut like.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&id,
|
|
||||||
)?;
|
|
||||||
like
|
|
||||||
.like_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(BaseBox::from_concrete(page)?)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
populate_object_props(
|
|
||||||
&mut undo.object_props,
|
|
||||||
vec![community.get_followers_url()],
|
|
||||||
&undo_id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(like)?;
|
|
||||||
|
|
||||||
send_activity_to_community(
|
|
||||||
&creator,
|
|
||||||
&community,
|
|
||||||
vec![community.get_shared_inbox_url()],
|
|
||||||
undo,
|
|
||||||
client,
|
|
||||||
pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,264 +0,0 @@
|
||||||
use crate::{
|
|
||||||
apub::{
|
|
||||||
activities::send_activity,
|
|
||||||
create_tombstone,
|
|
||||||
fetcher::get_or_fetch_and_upsert_remote_user,
|
|
||||||
ApubObjectType,
|
|
||||||
FromApub,
|
|
||||||
ToApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
convert_datetime,
|
|
||||||
db::{
|
|
||||||
activity::insert_activity,
|
|
||||||
private_message::{PrivateMessage, PrivateMessageForm},
|
|
||||||
user::User_,
|
|
||||||
Crud,
|
|
||||||
},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Create, Delete, Undo, Update},
|
|
||||||
context,
|
|
||||||
object::{kind::NoteType, properties::ObjectProperties, Note},
|
|
||||||
};
|
|
||||||
use activitystreams_new::object::Tombstone;
|
|
||||||
use actix_web::client::Client;
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ToApub for PrivateMessage {
|
|
||||||
type Response = Note;
|
|
||||||
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
|
||||||
let mut private_message = Note::default();
|
|
||||||
let oprops: &mut ObjectProperties = private_message.as_mut();
|
|
||||||
|
|
||||||
let creator_id = self.creator_id;
|
|
||||||
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
|
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
|
||||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
|
||||||
|
|
||||||
oprops
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(self.ap_id.to_owned())?
|
|
||||||
.set_published(convert_datetime(self.published))?
|
|
||||||
.set_content_xsd_string(self.content.to_owned())?
|
|
||||||
.set_to_xsd_any_uri(recipient.actor_id)?
|
|
||||||
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
|
|
||||||
|
|
||||||
if let Some(u) = self.updated {
|
|
||||||
oprops.set_updated(convert_datetime(u))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(private_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
create_tombstone(
|
|
||||||
self.deleted,
|
|
||||||
&self.ap_id,
|
|
||||||
self.updated,
|
|
||||||
NoteType.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for PrivateMessageForm {
|
|
||||||
type ApubType = Note;
|
|
||||||
|
|
||||||
/// Parse an ActivityPub note received from another instance into a Lemmy Private message
|
|
||||||
async fn from_apub(
|
|
||||||
note: &Note,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<PrivateMessageForm, LemmyError> {
|
|
||||||
let oprops = ¬e.object_props;
|
|
||||||
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
|
|
||||||
|
|
||||||
let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
|
|
||||||
|
|
||||||
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?;
|
|
||||||
|
|
||||||
Ok(PrivateMessageForm {
|
|
||||||
creator_id: creator.id,
|
|
||||||
recipient_id: recipient.id,
|
|
||||||
content: oprops
|
|
||||||
.get_content_xsd_string()
|
|
||||||
.map(|c| c.to_string())
|
|
||||||
.unwrap(),
|
|
||||||
published: oprops
|
|
||||||
.get_published()
|
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
|
||||||
updated: oprops
|
|
||||||
.get_updated()
|
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
|
||||||
deleted: None,
|
|
||||||
read: None,
|
|
||||||
ap_id: oprops.get_id().unwrap().to_string(),
|
|
||||||
local: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ApubObjectType for PrivateMessage {
|
|
||||||
/// Send out information about a newly created private message
|
|
||||||
async fn send_create(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
|
||||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
|
||||||
|
|
||||||
let mut create = Create::new();
|
|
||||||
create
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(id)?;
|
|
||||||
let to = format!("{}/inbox", recipient.actor_id);
|
|
||||||
|
|
||||||
create
|
|
||||||
.create_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
insert_activity(creator.id, create.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &create, creator, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send out information about an edited post, to the followers of the community.
|
|
||||||
async fn send_update(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
|
||||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
|
||||||
|
|
||||||
let mut update = Update::new();
|
|
||||||
update
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(id)?;
|
|
||||||
let to = format!("{}/inbox", recipient.actor_id);
|
|
||||||
|
|
||||||
update
|
|
||||||
.update_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
insert_activity(creator.id, update.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &update, creator, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
|
||||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
|
||||||
|
|
||||||
let mut delete = Delete::new();
|
|
||||||
delete
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(id)?;
|
|
||||||
let to = format!("{}/inbox", recipient.actor_id);
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
insert_activity(creator.id, delete.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &delete, creator, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
creator: &User_,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let note = self.to_apub(pool).await?;
|
|
||||||
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
|
||||||
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
|
|
||||||
|
|
||||||
let mut delete = Delete::new();
|
|
||||||
delete
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(id)?;
|
|
||||||
let to = format!("{}/inbox", recipient.actor_id);
|
|
||||||
|
|
||||||
delete
|
|
||||||
.delete_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(note)?;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::default();
|
|
||||||
|
|
||||||
undo
|
|
||||||
.object_props
|
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(undo_id)?;
|
|
||||||
|
|
||||||
undo
|
|
||||||
.undo_props
|
|
||||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
|
||||||
.set_object_base_box(delete)?;
|
|
||||||
|
|
||||||
insert_activity(creator.id, undo.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &undo, creator, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
_mod_: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
_mod_: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
use crate::api::community::GetCommunityResponse;
|
||||||
|
use crate::api::post::GetPostsResponse;
|
||||||
|
use crate::apub::*;
|
||||||
|
use crate::db::community_view::CommunityView;
|
||||||
|
use crate::db::post_view::PostView;
|
||||||
|
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use activitystreams::collection::{OrderedCollection, UnorderedCollection};
|
||||||
|
use activitystreams::object::Page;
|
||||||
|
use activitystreams::BaseBox;
|
||||||
|
use failure::Error;
|
||||||
|
use isahc::prelude::*;
|
||||||
|
use log::warn;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
fn fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
|
||||||
|
let well_known_uri = format!(
|
||||||
|
"{}://{}/.well-known/nodeinfo",
|
||||||
|
get_apub_protocol_string(),
|
||||||
|
domain
|
||||||
|
);
|
||||||
|
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
|
||||||
|
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_communities_from_instance(domain: &str) -> Result<Vec<CommunityView>, Error> {
|
||||||
|
let node_info = fetch_node_info(domain)?;
|
||||||
|
|
||||||
|
if let Some(community_list_url) = node_info.metadata.community_list_url {
|
||||||
|
let collection = fetch_remote_object::<UnorderedCollection>(&community_list_url)?;
|
||||||
|
let object_boxes = collection
|
||||||
|
.collection_props
|
||||||
|
.get_many_items_base_boxes()
|
||||||
|
.unwrap();
|
||||||
|
let communities: Result<Vec<CommunityView>, Error> = object_boxes
|
||||||
|
.map(|c| -> Result<CommunityView, Error> {
|
||||||
|
let group = c.to_owned().to_concrete::<GroupExt>()?;
|
||||||
|
CommunityView::from_group(&group, domain)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(communities?)
|
||||||
|
} else {
|
||||||
|
Err(format_err!(
|
||||||
|
"{} is not a Lemmy instance, federation is not supported",
|
||||||
|
domain
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_remote_object<Response>(uri: &str) -> Result<Response, Error>
|
||||||
|
where
|
||||||
|
Response: for<'de> Deserialize<'de>,
|
||||||
|
{
|
||||||
|
if Settings::get().federation.tls_enabled && !uri.starts_with("https://") {
|
||||||
|
return Err(format_err!("Activitypub uri is insecure: {}", uri));
|
||||||
|
}
|
||||||
|
// TODO: should cache responses here when we are in production
|
||||||
|
// TODO: this function should return a future
|
||||||
|
let text = isahc::get(uri)?.text()?;
|
||||||
|
let res: Response = serde_json::from_str(&text)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_remote_community_posts(identifier: &str) -> Result<GetPostsResponse, Error> {
|
||||||
|
let community = fetch_remote_object::<GroupExt>(&get_remote_community_uri(identifier))?;
|
||||||
|
let outbox_uri = &community.extension.get_outbox().to_string();
|
||||||
|
let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
|
||||||
|
let items = outbox.collection_props.get_many_items_base_boxes();
|
||||||
|
|
||||||
|
let posts: Result<Vec<PostView>, Error> = items
|
||||||
|
.unwrap()
|
||||||
|
.map(|obox: &BaseBox| {
|
||||||
|
let page = obox.clone().to_concrete::<Page>().unwrap();
|
||||||
|
PostView::from_page(&page)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(GetPostsResponse { posts: posts? })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_remote_community(identifier: &str) -> Result<GetCommunityResponse, failure::Error> {
|
||||||
|
let group = fetch_remote_object::<GroupExt>(&get_remote_community_uri(identifier))?;
|
||||||
|
// TODO: this is only for testing until we can call that function from GetPosts
|
||||||
|
// (once string ids are supported)
|
||||||
|
//dbg!(get_remote_community_posts(identifier)?);
|
||||||
|
|
||||||
|
let (_, domain) = split_identifier(identifier);
|
||||||
|
Ok(GetCommunityResponse {
|
||||||
|
moderators: vec![],
|
||||||
|
admins: vec![],
|
||||||
|
community: CommunityView::from_group(&group, &domain)?,
|
||||||
|
online: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_all_communities() -> Result<Vec<CommunityView>, Error> {
|
||||||
|
let mut communities_list: Vec<CommunityView> = vec![];
|
||||||
|
for instance in &get_following_instances() {
|
||||||
|
match fetch_communities_from_instance(instance) {
|
||||||
|
Ok(mut c) => communities_list.append(c.as_mut()),
|
||||||
|
Err(e) => warn!("Failed to fetch instance list from remote instance: {}", e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(communities_list)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,38 +1,19 @@
|
||||||
use crate::{
|
use crate::apub::{create_apub_response, make_apub_endpoint, EndpointType};
|
||||||
apub::{
|
use crate::convert_datetime;
|
||||||
activities::send_activity,
|
use crate::db::user::User_;
|
||||||
create_apub_response,
|
|
||||||
extensions::signatures::PublicKey,
|
|
||||||
ActorType,
|
|
||||||
FromApub,
|
|
||||||
PersonExt,
|
|
||||||
ToApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
convert_datetime,
|
|
||||||
db::{
|
|
||||||
activity::insert_activity,
|
|
||||||
user::{UserForm, User_},
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
routes::DbPoolParam,
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
actor::{properties::ApActorProperties, Person},
|
actor::{properties::ApActorProperties, Person},
|
||||||
context,
|
context,
|
||||||
endpoint::EndpointProperties,
|
ext::Extensible,
|
||||||
object::{properties::ObjectProperties, AnyImage, Image},
|
object::properties::ObjectProperties,
|
||||||
primitives::XsdAnyUri,
|
|
||||||
};
|
};
|
||||||
use activitystreams_ext::Ext2;
|
use actix_web::body::Body;
|
||||||
use activitystreams_new::{
|
use actix_web::web::Path;
|
||||||
activity::{Follow, Undo},
|
use actix_web::HttpResponse;
|
||||||
object::Tombstone,
|
use actix_web::{web, Result};
|
||||||
prelude::*,
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
};
|
use diesel::PgConnection;
|
||||||
use actix_web::{body::Body, client::Client, web, HttpResponse};
|
use failure::Error;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -40,223 +21,35 @@ pub struct UserQuery {
|
||||||
user_name: String,
|
user_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
pub async fn get_apub_user(
|
||||||
impl ToApub for User_ {
|
info: Path<UserQuery>,
|
||||||
type Response = PersonExt;
|
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||||
|
) -> Result<HttpResponse<Body>, Error> {
|
||||||
|
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
|
||||||
|
let base_url = make_apub_endpoint(EndpointType::User, &user.name);
|
||||||
|
|
||||||
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
|
let mut person = Person::default();
|
||||||
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
|
let oprops: &mut ObjectProperties = person.as_mut();
|
||||||
// TODO go through all these to_string and to_owned()
|
oprops
|
||||||
let mut person = Person::default();
|
.set_context_xsd_any_uri(context())?
|
||||||
let oprops: &mut ObjectProperties = person.as_mut();
|
.set_id(base_url.to_string())?
|
||||||
oprops
|
.set_published(convert_datetime(user.published))?;
|
||||||
.set_context_xsd_any_uri(context())?
|
|
||||||
.set_id(self.actor_id.to_string())?
|
|
||||||
.set_name_xsd_string(self.name.to_owned())?
|
|
||||||
.set_published(convert_datetime(self.published))?;
|
|
||||||
|
|
||||||
if let Some(u) = self.updated {
|
if let Some(u) = user.updated {
|
||||||
oprops.set_updated(convert_datetime(u))?;
|
oprops.set_updated(convert_datetime(u))?;
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(i) = &self.preferred_username {
|
|
||||||
oprops.set_name_xsd_string(i.to_owned())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(avatar_url) = &self.avatar {
|
|
||||||
let mut image = Image::new();
|
|
||||||
image
|
|
||||||
.object_props
|
|
||||||
.set_url_xsd_any_uri(avatar_url.to_owned())?;
|
|
||||||
let any_image = AnyImage::from_concrete(image)?;
|
|
||||||
oprops.set_icon_any_image(any_image)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut endpoint_props = EndpointProperties::default();
|
|
||||||
|
|
||||||
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
|
|
||||||
|
|
||||||
let mut actor_props = ApActorProperties::default();
|
|
||||||
|
|
||||||
actor_props
|
|
||||||
.set_inbox(self.get_inbox_url())?
|
|
||||||
.set_outbox(self.get_outbox_url())?
|
|
||||||
.set_endpoints(endpoint_props)?
|
|
||||||
.set_followers(self.get_followers_url())?
|
|
||||||
.set_following(self.get_following_url())?
|
|
||||||
.set_liked(self.get_liked_url())?;
|
|
||||||
|
|
||||||
Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
|
|
||||||
}
|
}
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
unimplemented!()
|
if let Some(i) = &user.preferred_username {
|
||||||
|
oprops.set_name_xsd_string(i.to_owned())?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let mut actor_props = ApActorProperties::default();
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl ActorType for User_ {
|
actor_props
|
||||||
fn actor_id(&self) -> String {
|
.set_inbox(format!("{}/inbox", &base_url))?
|
||||||
self.actor_id.to_owned()
|
.set_outbox(format!("{}/outbox", &base_url))?
|
||||||
}
|
.set_following(format!("{}/following", &base_url))?
|
||||||
|
.set_liked(format!("{}/liked", &base_url))?;
|
||||||
fn public_key(&self) -> String {
|
|
||||||
self.public_key.to_owned().unwrap()
|
Ok(create_apub_response(&person.extend(actor_props)))
|
||||||
}
|
|
||||||
|
|
||||||
fn private_key(&self) -> String {
|
|
||||||
self.private_key.to_owned().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// As a given local user, send out a follow request to a remote community.
|
|
||||||
async fn send_follow(
|
|
||||||
&self,
|
|
||||||
follow_actor_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
|
|
||||||
follow.set_context(context()).set_id(id.parse()?);
|
|
||||||
let to = format!("{}/inbox", follow_actor_id);
|
|
||||||
|
|
||||||
insert_activity(self.id, follow.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &follow, self, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_unfollow(
|
|
||||||
&self,
|
|
||||||
follow_actor_id: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
|
|
||||||
follow.set_context(context()).set_id(id.parse()?);
|
|
||||||
|
|
||||||
let to = format!("{}/inbox", follow_actor_id);
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Undo that fake activity
|
|
||||||
let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4());
|
|
||||||
let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?);
|
|
||||||
undo.set_context(context()).set_id(undo_id.parse()?);
|
|
||||||
|
|
||||||
insert_activity(self.id, undo.clone(), true, pool).await?;
|
|
||||||
|
|
||||||
send_activity(client, &undo, self, vec![to]).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_delete(
|
|
||||||
&self,
|
|
||||||
_creator: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_delete(
|
|
||||||
&self,
|
|
||||||
_creator: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_remove(
|
|
||||||
&self,
|
|
||||||
_creator: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_undo_remove(
|
|
||||||
&self,
|
|
||||||
_creator: &User_,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_accept_follow(
|
|
||||||
&self,
|
|
||||||
_follow: &Follow,
|
|
||||||
_client: &Client,
|
|
||||||
_pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<String>, LemmyError> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for UserForm {
|
|
||||||
type ApubType = PersonExt;
|
|
||||||
/// Parse an ActivityPub person received from another instance into a Lemmy user.
|
|
||||||
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
|
|
||||||
let oprops = &person.inner.object_props;
|
|
||||||
let aprops = &person.ext_one;
|
|
||||||
let public_key: &PublicKey = &person.ext_two.public_key;
|
|
||||||
|
|
||||||
let avatar = match oprops.get_icon_any_image() {
|
|
||||||
Some(any_image) => any_image
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Image>()?
|
|
||||||
.object_props
|
|
||||||
.get_url_xsd_any_uri()
|
|
||||||
.map(|u| u.to_string()),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(UserForm {
|
|
||||||
name: oprops.get_name_xsd_string().unwrap().to_string(),
|
|
||||||
preferred_username: aprops.get_preferred_username().map(|u| u.to_string()),
|
|
||||||
password_encrypted: "".to_string(),
|
|
||||||
admin: false,
|
|
||||||
banned: false,
|
|
||||||
email: None,
|
|
||||||
avatar,
|
|
||||||
updated: oprops
|
|
||||||
.get_updated()
|
|
||||||
.map(|u| u.as_ref().to_owned().naive_local()),
|
|
||||||
show_nsfw: false,
|
|
||||||
theme: "".to_string(),
|
|
||||||
default_sort_type: 0,
|
|
||||||
default_listing_type: 0,
|
|
||||||
lang: "".to_string(),
|
|
||||||
show_avatars: false,
|
|
||||||
send_notifications_to_email: false,
|
|
||||||
matrix_user_id: None,
|
|
||||||
actor_id: oprops.get_id().unwrap().to_string(),
|
|
||||||
bio: oprops.get_summary_xsd_string().map(|s| s.to_string()),
|
|
||||||
local: false,
|
|
||||||
private_key: None,
|
|
||||||
public_key: Some(public_key.to_owned().public_key_pem),
|
|
||||||
last_refreshed_at: Some(naive_now()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the user json over HTTP.
|
|
||||||
pub async fn get_apub_user_http(
|
|
||||||
info: web::Path<UserQuery>,
|
|
||||||
db: DbPoolParam,
|
|
||||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
|
||||||
let user_name = info.into_inner().user_name;
|
|
||||||
let user = blocking(&db, move |conn| {
|
|
||||||
User_::find_by_email_or_username(conn, &user_name)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
let u = user.to_apub(&db).await?;
|
|
||||||
Ok(create_apub_response(&u))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,372 +0,0 @@
|
||||||
use crate::{
|
|
||||||
api::user::PrivateMessageResponse,
|
|
||||||
apub::{
|
|
||||||
extensions::signatures::verify,
|
|
||||||
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
|
|
||||||
FromApub,
|
|
||||||
},
|
|
||||||
blocking,
|
|
||||||
db::{
|
|
||||||
activity::insert_activity,
|
|
||||||
community::{CommunityFollower, CommunityFollowerForm},
|
|
||||||
private_message::{PrivateMessage, PrivateMessageForm},
|
|
||||||
private_message_view::PrivateMessageView,
|
|
||||||
user::User_,
|
|
||||||
Crud,
|
|
||||||
Followable,
|
|
||||||
},
|
|
||||||
naive_now,
|
|
||||||
routes::{ChatServerParam, DbPoolParam},
|
|
||||||
websocket::{server::SendUserRoomMessage, UserOperation},
|
|
||||||
DbPool,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use activitystreams::{
|
|
||||||
activity::{Accept, Create, Delete, Undo, Update},
|
|
||||||
object::Note,
|
|
||||||
};
|
|
||||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
|
||||||
use log::debug;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub enum UserAcceptedObjects {
|
|
||||||
Accept(Box<Accept>),
|
|
||||||
Create(Box<Create>),
|
|
||||||
Update(Box<Update>),
|
|
||||||
Delete(Box<Delete>),
|
|
||||||
Undo(Box<Undo>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handler for all incoming activities to user inboxes.
|
|
||||||
pub async fn user_inbox(
|
|
||||||
request: HttpRequest,
|
|
||||||
input: web::Json<UserAcceptedObjects>,
|
|
||||||
path: web::Path<String>,
|
|
||||||
client: web::Data<Client>,
|
|
||||||
db: DbPoolParam,
|
|
||||||
chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
// TODO: would be nice if we could do the signature check here, but we cant access the actor property
|
|
||||||
let input = input.into_inner();
|
|
||||||
let username = path.into_inner();
|
|
||||||
debug!("User {} received activity: {:?}", &username, &input);
|
|
||||||
|
|
||||||
match input {
|
|
||||||
UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await,
|
|
||||||
UserAcceptedObjects::Create(c) => {
|
|
||||||
receive_create_private_message(*c, &request, &client, &db, chat_server).await
|
|
||||||
}
|
|
||||||
UserAcceptedObjects::Update(u) => {
|
|
||||||
receive_update_private_message(*u, &request, &client, &db, chat_server).await
|
|
||||||
}
|
|
||||||
UserAcceptedObjects::Delete(d) => {
|
|
||||||
receive_delete_private_message(*d, &request, &client, &db, chat_server).await
|
|
||||||
}
|
|
||||||
UserAcceptedObjects::Undo(u) => {
|
|
||||||
receive_undo_delete_private_message(*u, &request, &client, &db, chat_server).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle accepted follows.
|
|
||||||
async fn receive_accept(
|
|
||||||
accept: Accept,
|
|
||||||
request: &HttpRequest,
|
|
||||||
username: &str,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let community_uri = accept
|
|
||||||
.accept_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
|
|
||||||
verify(request, &community)?;
|
|
||||||
|
|
||||||
let username = username.to_owned();
|
|
||||||
let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await??;
|
|
||||||
|
|
||||||
insert_activity(community.creator_id, accept, false, pool).await?;
|
|
||||||
|
|
||||||
// Now you need to add this to the community follower
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
|
||||||
community_id: community.id,
|
|
||||||
user_id: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This will fail if they're already a follower
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
CommunityFollower::follow(conn, &community_follower_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
// TODO: make sure that we actually requested a follow
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_create_private_message(
|
|
||||||
create: Create,
|
|
||||||
request: &HttpRequest,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let note = create
|
|
||||||
.create_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Note>()?;
|
|
||||||
|
|
||||||
let user_uri = create
|
|
||||||
.create_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
|
||||||
verify(request, &user)?;
|
|
||||||
|
|
||||||
insert_activity(user.id, create, false, pool).await?;
|
|
||||||
|
|
||||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
|
||||||
|
|
||||||
let inserted_private_message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::create(conn, &private_message)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessageView::read(conn, inserted_private_message.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse { message };
|
|
||||||
|
|
||||||
let recipient_id = res.message.recipient_id;
|
|
||||||
|
|
||||||
chat_server.do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperation::CreatePrivateMessage,
|
|
||||||
response: res,
|
|
||||||
recipient_id,
|
|
||||||
my_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_update_private_message(
|
|
||||||
update: Update,
|
|
||||||
request: &HttpRequest,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let note = update
|
|
||||||
.update_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Note>()?;
|
|
||||||
|
|
||||||
let user_uri = update
|
|
||||||
.update_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
|
||||||
verify(request, &user)?;
|
|
||||||
|
|
||||||
insert_activity(user.id, update, false, pool).await?;
|
|
||||||
|
|
||||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
|
||||||
|
|
||||||
let private_message_ap_id = private_message_form.ap_id.clone();
|
|
||||||
let private_message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let private_message_id = private_message.id;
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::update(conn, private_message_id, &private_message_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let private_message_id = private_message.id;
|
|
||||||
let message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessageView::read(conn, private_message_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse { message };
|
|
||||||
|
|
||||||
let recipient_id = res.message.recipient_id;
|
|
||||||
|
|
||||||
chat_server.do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperation::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
recipient_id,
|
|
||||||
my_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_delete_private_message(
|
|
||||||
delete: Delete,
|
|
||||||
request: &HttpRequest,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let note = delete
|
|
||||||
.delete_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Note>()?;
|
|
||||||
|
|
||||||
let user_uri = delete
|
|
||||||
.delete_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
|
||||||
verify(request, &user)?;
|
|
||||||
|
|
||||||
insert_activity(user.id, delete, false, pool).await?;
|
|
||||||
|
|
||||||
let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
|
||||||
|
|
||||||
let private_message_ap_id = private_message_form.ap_id;
|
|
||||||
let private_message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let private_message_form = PrivateMessageForm {
|
|
||||||
content: private_message_form.content,
|
|
||||||
recipient_id: private_message.recipient_id,
|
|
||||||
creator_id: private_message.creator_id,
|
|
||||||
deleted: Some(true),
|
|
||||||
read: None,
|
|
||||||
ap_id: private_message.ap_id,
|
|
||||||
local: private_message.local,
|
|
||||||
published: None,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let private_message_id = private_message.id;
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::update(conn, private_message_id, &private_message_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let private_message_id = private_message.id;
|
|
||||||
let message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessageView::read(&conn, private_message_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse { message };
|
|
||||||
|
|
||||||
let recipient_id = res.message.recipient_id;
|
|
||||||
|
|
||||||
chat_server.do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperation::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
recipient_id,
|
|
||||||
my_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_undo_delete_private_message(
|
|
||||||
undo: Undo,
|
|
||||||
request: &HttpRequest,
|
|
||||||
client: &Client,
|
|
||||||
pool: &DbPool,
|
|
||||||
chat_server: ChatServerParam,
|
|
||||||
) -> Result<HttpResponse, LemmyError> {
|
|
||||||
let delete = undo
|
|
||||||
.undo_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Delete>()?;
|
|
||||||
|
|
||||||
let note = delete
|
|
||||||
.delete_props
|
|
||||||
.get_object_base_box()
|
|
||||||
.to_owned()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned()
|
|
||||||
.into_concrete::<Note>()?;
|
|
||||||
|
|
||||||
let user_uri = delete
|
|
||||||
.delete_props
|
|
||||||
.get_actor_xsd_any_uri()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
|
|
||||||
verify(request, &user)?;
|
|
||||||
|
|
||||||
insert_activity(user.id, delete, false, pool).await?;
|
|
||||||
|
|
||||||
let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
|
|
||||||
|
|
||||||
let private_message_ap_id = private_message.ap_id.clone();
|
|
||||||
let private_message_id = blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id).map(|pm| pm.id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let private_message_form = PrivateMessageForm {
|
|
||||||
content: private_message.content,
|
|
||||||
recipient_id: private_message.recipient_id,
|
|
||||||
creator_id: private_message.creator_id,
|
|
||||||
deleted: Some(false),
|
|
||||||
read: None,
|
|
||||||
ap_id: private_message.ap_id,
|
|
||||||
local: private_message.local,
|
|
||||||
published: None,
|
|
||||||
updated: Some(naive_now()),
|
|
||||||
};
|
|
||||||
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
PrivateMessage::update(conn, private_message_id, &private_message_form)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let message = blocking(pool, move |conn| {
|
|
||||||
PrivateMessageView::read(&conn, private_message_id)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
|
|
||||||
let res = PrivateMessageResponse { message };
|
|
||||||
|
|
||||||
let recipient_id = res.message.recipient_id;
|
|
||||||
|
|
||||||
chat_server.do_send(SendUserRoomMessage {
|
|
||||||
op: UserOperation::EditPrivateMessage,
|
|
||||||
response: res,
|
|
||||||
recipient_id,
|
|
||||||
my_id: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError};
|
|
||||||
use diesel::{dsl::*, result::Error, *};
|
|
||||||
use log::debug;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
|
||||||
#[table_name = "activity"]
|
|
||||||
pub struct Activity {
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub data: Value,
|
|
||||||
pub local: bool,
|
|
||||||
pub published: chrono::NaiveDateTime,
|
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
|
||||||
#[table_name = "activity"]
|
|
||||||
pub struct ActivityForm {
|
|
||||||
pub user_id: i32,
|
|
||||||
pub data: Value,
|
|
||||||
pub local: bool,
|
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Crud<ActivityForm> for Activity {
|
|
||||||
fn read(conn: &PgConnection, activity_id: i32) -> Result<Self, Error> {
|
|
||||||
use crate::schema::activity::dsl::*;
|
|
||||||
activity.find(activity_id).first::<Self>(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete(conn: &PgConnection, activity_id: i32) -> Result<usize, Error> {
|
|
||||||
use crate::schema::activity::dsl::*;
|
|
||||||
diesel::delete(activity.find(activity_id)).execute(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create(conn: &PgConnection, new_activity: &ActivityForm) -> Result<Self, Error> {
|
|
||||||
use crate::schema::activity::dsl::*;
|
|
||||||
insert_into(activity)
|
|
||||||
.values(new_activity)
|
|
||||||
.get_result::<Self>(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
conn: &PgConnection,
|
|
||||||
activity_id: i32,
|
|
||||||
new_activity: &ActivityForm,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
use crate::schema::activity::dsl::*;
|
|
||||||
diesel::update(activity.find(activity_id))
|
|
||||||
.set(new_activity)
|
|
||||||
.get_result::<Self>(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_activity<T>(
|
|
||||||
user_id: i32,
|
|
||||||
data: T,
|
|
||||||
local: bool,
|
|
||||||
pool: &DbPool,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: Serialize + Debug + Send + 'static,
|
|
||||||
{
|
|
||||||
blocking(pool, move |conn| {
|
|
||||||
do_insert_activity(conn, user_id, &data, local)
|
|
||||||
})
|
|
||||||
.await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_insert_activity<T>(
|
|
||||||
conn: &PgConnection,
|
|
||||||
user_id: i32,
|
|
||||||
data: &T,
|
|
||||||
local: bool,
|
|
||||||
) -> Result<(), LemmyError>
|
|
||||||
where
|
|
||||||
T: Serialize + Debug,
|
|
||||||
{
|
|
||||||
let activity_form = ActivityForm {
|
|
||||||
user_id,
|
|
||||||
data: serde_json::to_value(&data)?,
|
|
||||||
local,
|
|
||||||
updated: None,
|
|
||||||
};
|
|
||||||
debug!("inserting activity for user {}, data {:?}", user_id, data);
|
|
||||||
Activity::create(&conn, &activity_form)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{super::user::*, *};
|
|
||||||
use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_crud() {
|
|
||||||
let conn = establish_unpooled_connection();
|
|
||||||
|
|
||||||
let creator_form = UserForm {
|
|
||||||
name: "activity_creator_pm".into(),
|
|
||||||
preferred_username: None,
|
|
||||||
password_encrypted: "nope".into(),
|
|
||||||
email: None,
|
|
||||||
matrix_user_id: None,
|
|
||||||
avatar: None,
|
|
||||||
admin: false,
|
|
||||||
banned: false,
|
|
||||||
updated: None,
|
|
||||||
show_nsfw: false,
|
|
||||||
theme: "darkly".into(),
|
|
||||||
default_sort_type: SortType::Hot as i16,
|
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
|
||||||
lang: "browser".into(),
|
|
||||||
show_avatars: true,
|
|
||||||
send_notifications_to_email: false,
|
|
||||||
actor_id: "http://fake.com".into(),
|
|
||||||
bio: None,
|
|
||||||
local: true,
|
|
||||||
private_key: None,
|
|
||||||
public_key: None,
|
|
||||||
last_refreshed_at: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
|
|
||||||
|
|
||||||
let test_json: Value = serde_json::from_str(
|
|
||||||
r#"{
|
|
||||||
"street": "Article Circle Expressway 1",
|
|
||||||
"city": "North Pole",
|
|
||||||
"postcode": "99705",
|
|
||||||
"state": "Alaska"
|
|
||||||
}"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let activity_form = ActivityForm {
|
|
||||||
user_id: inserted_creator.id,
|
|
||||||
data: test_json.to_owned(),
|
|
||||||
local: true,
|
|
||||||
updated: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let inserted_activity = Activity::create(&conn, &activity_form).unwrap();
|
|
||||||
|
|
||||||
let expected_activity = Activity {
|
|
||||||
id: inserted_activity.id,
|
|
||||||
user_id: inserted_creator.id,
|
|
||||||
data: test_json,
|
|
||||||
local: true,
|
|
||||||
published: inserted_activity.published,
|
|
||||||
updated: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let read_activity = Activity::read(&conn, inserted_activity.id).unwrap();
|
|
||||||
let num_deleted = Activity::delete(&conn, inserted_activity.id).unwrap();
|
|
||||||
User_::delete(&conn, inserted_creator.id).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(expected_activity, read_activity);
|
|
||||||
assert_eq!(expected_activity, inserted_activity);
|
|
||||||
assert_eq!(1, num_deleted);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::{
|
use super::*;
|
||||||
db::Crud,
|
use crate::schema::category;
|
||||||
schema::{category, category::dsl::*},
|
use crate::schema::category::dsl::*;
|
||||||
};
|
|
||||||
use diesel::{dsl::*, result::Error, *};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[table_name = "category"]
|
#[table_name = "category"]
|
||||||
|
@ -53,8 +50,6 @@ impl Category {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::db::establish_unpooled_connection;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_unpooled_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
|
|
@ -1,45 +1,35 @@
|
||||||
// This is for db migrations that require code
|
// This is for db migrations that require code
|
||||||
use super::{
|
use super::comment::Comment;
|
||||||
comment::Comment,
|
use super::community::{Community, CommunityForm};
|
||||||
community::{Community, CommunityForm},
|
use super::post::Post;
|
||||||
post::Post,
|
use super::user::{UserForm, User_};
|
||||||
private_message::PrivateMessage,
|
use super::*;
|
||||||
user::{UserForm, User_},
|
use crate::apub::{gen_keypair_str, make_apub_endpoint, EndpointType};
|
||||||
};
|
use crate::naive_now;
|
||||||
use crate::{
|
|
||||||
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
|
|
||||||
db::Crud,
|
|
||||||
naive_now,
|
|
||||||
LemmyError,
|
|
||||||
};
|
|
||||||
use diesel::*;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
|
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> {
|
||||||
user_updates_2020_04_02(&conn)?;
|
user_updates_2020_04_02(conn)?;
|
||||||
community_updates_2020_04_02(&conn)?;
|
community_updates_2020_04_02(conn)?;
|
||||||
post_updates_2020_04_03(&conn)?;
|
post_updates_2020_04_03(conn)?;
|
||||||
comment_updates_2020_04_03(&conn)?;
|
comment_updates_2020_04_03(conn)?;
|
||||||
private_message_updates_2020_05_05(&conn)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
|
||||||
use crate::schema::user_::dsl::*;
|
use crate::schema::user_::dsl::*;
|
||||||
|
|
||||||
info!("Running user_updates_2020_04_02");
|
info!("Running user_updates_2020_04_02");
|
||||||
|
|
||||||
// Update the actor_id, private_key, and public_key, last_refreshed_at
|
// Update the actor_id, private_key, and public_key, last_refreshed_at
|
||||||
let incorrect_users = user_
|
let incorrect_users = user_
|
||||||
.filter(actor_id.eq("http://fake.com"))
|
.filter(actor_id.eq("changeme"))
|
||||||
.filter(local.eq(true))
|
.filter(local.eq(true))
|
||||||
.load::<User_>(conn)?;
|
.load::<User_>(conn)?;
|
||||||
|
|
||||||
sql_query("alter table user_ disable trigger refresh_user").execute(conn)?;
|
|
||||||
|
|
||||||
for cuser in &incorrect_users {
|
for cuser in &incorrect_users {
|
||||||
let keypair = generate_actor_keypair()?;
|
let (user_public_key, user_private_key) = gen_keypair_str();
|
||||||
|
|
||||||
let form = UserForm {
|
let form = UserForm {
|
||||||
name: cuser.name.to_owned(),
|
name: cuser.name.to_owned(),
|
||||||
|
@ -61,36 +51,32 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
||||||
actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(),
|
actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(),
|
||||||
bio: cuser.bio.to_owned(),
|
bio: cuser.bio.to_owned(),
|
||||||
local: cuser.local,
|
local: cuser.local,
|
||||||
private_key: Some(keypair.private_key),
|
private_key: Some(user_private_key),
|
||||||
public_key: Some(keypair.public_key),
|
public_key: Some(user_public_key),
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
};
|
};
|
||||||
|
|
||||||
User_::update(&conn, cuser.id, &form)?;
|
User_::update(&conn, cuser.id, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sql_query("alter table user_ enable trigger refresh_user").execute(conn)?;
|
|
||||||
|
|
||||||
info!("{} user rows updated.", incorrect_users.len());
|
info!("{} user rows updated.", incorrect_users.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
|
|
||||||
info!("Running community_updates_2020_04_02");
|
info!("Running community_updates_2020_04_02");
|
||||||
|
|
||||||
// Update the actor_id, private_key, and public_key, last_refreshed_at
|
// Update the actor_id, private_key, and public_key, last_refreshed_at
|
||||||
let incorrect_communities = community
|
let incorrect_communities = community
|
||||||
.filter(actor_id.eq("http://fake.com"))
|
.filter(actor_id.eq("changeme"))
|
||||||
.filter(local.eq(true))
|
.filter(local.eq(true))
|
||||||
.load::<Community>(conn)?;
|
.load::<Community>(conn)?;
|
||||||
|
|
||||||
sql_query("alter table community disable trigger refresh_community").execute(conn)?;
|
|
||||||
|
|
||||||
for ccommunity in &incorrect_communities {
|
for ccommunity in &incorrect_communities {
|
||||||
let keypair = generate_actor_keypair()?;
|
let (community_public_key, community_private_key) = gen_keypair_str();
|
||||||
|
|
||||||
let form = CommunityForm {
|
let form = CommunityForm {
|
||||||
name: ccommunity.name.to_owned(),
|
name: ccommunity.name.to_owned(),
|
||||||
|
@ -104,90 +90,55 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
||||||
updated: None,
|
updated: None,
|
||||||
actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(),
|
actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(),
|
||||||
local: ccommunity.local,
|
local: ccommunity.local,
|
||||||
private_key: Some(keypair.private_key),
|
private_key: Some(community_private_key),
|
||||||
public_key: Some(keypair.public_key),
|
public_key: Some(community_public_key),
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Community::update(&conn, ccommunity.id, &form)?;
|
Community::update(&conn, ccommunity.id, &form)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sql_query("alter table community enable trigger refresh_community").execute(conn)?;
|
|
||||||
|
|
||||||
info!("{} community rows updated.", incorrect_communities.len());
|
info!("{} community rows updated.", incorrect_communities.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
|
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
|
||||||
use crate::schema::post::dsl::*;
|
use crate::schema::post::dsl::*;
|
||||||
|
|
||||||
info!("Running post_updates_2020_04_03");
|
info!("Running post_updates_2020_04_03");
|
||||||
|
|
||||||
// Update the ap_id
|
// Update the ap_id
|
||||||
let incorrect_posts = post
|
let incorrect_posts = post
|
||||||
.filter(ap_id.eq("http://fake.com"))
|
.filter(ap_id.eq("changeme"))
|
||||||
.filter(local.eq(true))
|
.filter(local.eq(true))
|
||||||
.load::<Post>(conn)?;
|
.load::<Post>(conn)?;
|
||||||
|
|
||||||
sql_query("alter table post disable trigger refresh_post").execute(conn)?;
|
|
||||||
|
|
||||||
for cpost in &incorrect_posts {
|
for cpost in &incorrect_posts {
|
||||||
Post::update_ap_id(&conn, cpost.id)?;
|
Post::update_ap_id(&conn, cpost.id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("{} post rows updated.", incorrect_posts.len());
|
info!("{} post rows updated.", incorrect_posts.len());
|
||||||
|
|
||||||
sql_query("alter table post enable trigger refresh_post").execute(conn)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
|
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
|
||||||
use crate::schema::comment::dsl::*;
|
use crate::schema::comment::dsl::*;
|
||||||
|
|
||||||
info!("Running comment_updates_2020_04_03");
|
info!("Running comment_updates_2020_04_03");
|
||||||
|
|
||||||
// Update the ap_id
|
// Update the ap_id
|
||||||
let incorrect_comments = comment
|
let incorrect_comments = comment
|
||||||
.filter(ap_id.eq("http://fake.com"))
|
.filter(ap_id.eq("changeme"))
|
||||||
.filter(local.eq(true))
|
.filter(local.eq(true))
|
||||||
.load::<Comment>(conn)?;
|
.load::<Comment>(conn)?;
|
||||||
|
|
||||||
sql_query("alter table comment disable trigger refresh_comment").execute(conn)?;
|
|
||||||
|
|
||||||
for ccomment in &incorrect_comments {
|
for ccomment in &incorrect_comments {
|
||||||
Comment::update_ap_id(&conn, ccomment.id)?;
|
Comment::update_ap_id(&conn, ccomment.id)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sql_query("alter table comment enable trigger refresh_comment").execute(conn)?;
|
|
||||||
|
|
||||||
info!("{} comment rows updated.", incorrect_comments.len());
|
info!("{} comment rows updated.", incorrect_comments.len());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
|
|
||||||
use crate::schema::private_message::dsl::*;
|
|
||||||
|
|
||||||
info!("Running private_message_updates_2020_05_05");
|
|
||||||
|
|
||||||
// Update the ap_id
|
|
||||||
let incorrect_pms = private_message
|
|
||||||
.filter(ap_id.eq("http://fake.com"))
|
|
||||||
.filter(local.eq(true))
|
|
||||||
.load::<PrivateMessage>(conn)?;
|
|
||||||
|
|
||||||
sql_query("alter table private_message disable trigger refresh_private_message").execute(conn)?;
|
|
||||||
|
|
||||||
for cpm in &incorrect_pms {
|
|
||||||
PrivateMessage::update_ap_id(&conn, cpm.id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
sql_query("alter table private_message enable trigger refresh_private_message").execute(conn)?;
|
|
||||||
|
|
||||||
info!("{} private message rows updated.", incorrect_pms.len());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use super::{post::Post, *};
|
use super::post::Post;
|
||||||
use crate::{
|
use super::*;
|
||||||
apub::{make_apub_endpoint, EndpointType},
|
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||||
naive_now,
|
use crate::naive_now;
|
||||||
schema::{comment, comment_like, comment_saved},
|
use crate::schema::{comment, comment_like, comment_saved};
|
||||||
};
|
|
||||||
|
|
||||||
// WITH RECURSIVE MyTree AS (
|
// WITH RECURSIVE MyTree AS (
|
||||||
// SELECT * FROM comment WHERE parent_id IS NULL
|
// SELECT * FROM comment WHERE parent_id IS NULL
|
||||||
|
@ -12,7 +11,7 @@ use crate::{
|
||||||
// )
|
// )
|
||||||
// SELECT * FROM MyTree;
|
// SELECT * FROM MyTree;
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[belongs_to(Post)]
|
#[belongs_to(Post)]
|
||||||
#[table_name = "comment"]
|
#[table_name = "comment"]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
|
@ -22,7 +21,7 @@ pub struct Comment {
|
||||||
pub parent_id: Option<i32>,
|
pub parent_id: Option<i32>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub removed: bool,
|
pub removed: bool,
|
||||||
pub read: bool, // Whether the recipient has read the comment or not
|
pub read: bool,
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
|
@ -39,7 +38,6 @@ pub struct CommentForm {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub read: Option<bool>,
|
pub read: Option<bool>,
|
||||||
pub published: Option<chrono::NaiveDateTime>,
|
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: Option<bool>,
|
pub deleted: Option<bool>,
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
|
@ -86,11 +84,6 @@ impl Comment {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
|
|
||||||
use crate::schema::comment::dsl::*;
|
|
||||||
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
|
||||||
use crate::schema::comment::dsl::*;
|
use crate::schema::comment::dsl::*;
|
||||||
|
|
||||||
|
@ -204,10 +197,10 @@ impl Saveable<CommentSavedForm> for CommentSaved {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::super::community::*;
|
||||||
super::{community::*, post::*, user::*},
|
use super::super::post::*;
|
||||||
*,
|
use super::super::user::*;
|
||||||
};
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_unpooled_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
@ -229,7 +222,7 @@ mod tests {
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
show_avatars: true,
|
show_avatars: true,
|
||||||
send_notifications_to_email: false,
|
send_notifications_to_email: false,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
bio: None,
|
bio: None,
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
|
@ -249,12 +242,11 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -275,9 +267,8 @@ mod tests {
|
||||||
embed_description: None,
|
embed_description: None,
|
||||||
embed_html: None,
|
embed_html: None,
|
||||||
thumbnail_url: None,
|
thumbnail_url: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||||
|
@ -290,9 +281,8 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
published: None,
|
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -309,7 +299,7 @@ mod tests {
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -321,9 +311,8 @@ mod tests {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
published: None,
|
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
|
use super::*;
|
||||||
use diesel::{dsl::*, pg::Pg, result::Error, *};
|
use diesel::pg::Pg;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
// The faked schema since diesel doesn't do views
|
// The faked schema since diesel doesn't do views
|
||||||
table! {
|
table! {
|
||||||
|
@ -15,16 +14,10 @@ table! {
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
ap_id -> Text,
|
|
||||||
local -> Bool,
|
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
community_actor_id -> Text,
|
|
||||||
community_local -> Bool,
|
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
|
||||||
creator_local -> Bool,
|
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
|
@ -50,16 +43,10 @@ table! {
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
ap_id -> Text,
|
|
||||||
local -> Bool,
|
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
community_actor_id -> Text,
|
|
||||||
community_local -> Bool,
|
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
|
||||||
creator_local -> Bool,
|
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
|
@ -88,16 +75,10 @@ pub struct CommentView {
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub ap_id: String,
|
|
||||||
pub local: bool,
|
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub community_actor_id: String,
|
|
||||||
pub community_local: bool,
|
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_actor_id: String,
|
|
||||||
pub creator_local: bool,
|
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
|
@ -301,16 +282,10 @@ table! {
|
||||||
published -> Timestamp,
|
published -> Timestamp,
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
ap_id -> Text,
|
|
||||||
local -> Bool,
|
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
community_actor_id -> Text,
|
|
||||||
community_local -> Bool,
|
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
|
||||||
creator_local -> Bool,
|
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
|
@ -340,16 +315,10 @@ pub struct ReplyView {
|
||||||
pub published: chrono::NaiveDateTime,
|
pub published: chrono::NaiveDateTime,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub ap_id: String,
|
|
||||||
pub local: bool,
|
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub community_actor_id: String,
|
|
||||||
pub community_local: bool,
|
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_actor_id: String,
|
|
||||||
pub creator_local: bool,
|
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
|
@ -454,12 +423,11 @@ impl<'a> ReplyQueryBuilder<'a> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::super::comment::*;
|
||||||
super::{comment::*, community::*, post::*, user::*},
|
use super::super::community::*;
|
||||||
*,
|
use super::super::post::*;
|
||||||
};
|
use super::super::user::*;
|
||||||
use crate::db::{establish_unpooled_connection, Crud, Likeable};
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_unpooled_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
@ -481,7 +449,7 @@ mod tests {
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
show_avatars: true,
|
show_avatars: true,
|
||||||
send_notifications_to_email: false,
|
send_notifications_to_email: false,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
bio: None,
|
bio: None,
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
|
@ -501,12 +469,11 @@ mod tests {
|
||||||
deleted: None,
|
deleted: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -527,9 +494,8 @@ mod tests {
|
||||||
embed_description: None,
|
embed_description: None,
|
||||||
embed_html: None,
|
embed_html: None,
|
||||||
thumbnail_url: None,
|
thumbnail_url: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
let inserted_post = Post::create(&conn, &new_post).unwrap();
|
||||||
|
@ -542,9 +508,8 @@ mod tests {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
read: None,
|
read: None,
|
||||||
published: None,
|
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: "http://fake.com".into(),
|
ap_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -584,12 +549,6 @@ mod tests {
|
||||||
my_vote: None,
|
my_vote: None,
|
||||||
subscribed: None,
|
subscribed: None,
|
||||||
saved: None,
|
saved: None,
|
||||||
ap_id: "http://fake.com".to_string(),
|
|
||||||
local: true,
|
|
||||||
community_actor_id: inserted_community.actor_id.to_owned(),
|
|
||||||
community_local: true,
|
|
||||||
creator_actor_id: inserted_user.actor_id.to_owned(),
|
|
||||||
creator_local: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let expected_comment_view_with_user = CommentView {
|
let expected_comment_view_with_user = CommentView {
|
||||||
|
@ -617,12 +576,6 @@ mod tests {
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
subscribed: None,
|
subscribed: None,
|
||||||
saved: None,
|
saved: None,
|
||||||
ap_id: "http://fake.com".to_string(),
|
|
||||||
local: true,
|
|
||||||
community_actor_id: inserted_community.actor_id.to_owned(),
|
|
||||||
community_local: true,
|
|
||||||
creator_actor_id: inserted_user.actor_id.to_owned(),
|
|
||||||
creator_local: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)
|
let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
use crate::{
|
use super::*;
|
||||||
db::{Bannable, Crud, Followable, Joinable},
|
use crate::schema::{community, community_follower, community_moderator, community_user_ban};
|
||||||
schema::{community, community_follower, community_moderator, community_user_ban},
|
|
||||||
};
|
|
||||||
use diesel::{dsl::*, result::Error, *};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
#[table_name = "community"]
|
#[table_name = "community"]
|
||||||
pub struct Community {
|
pub struct Community {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
@ -26,8 +22,7 @@ pub struct Community {
|
||||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add better delete, remove, lock actions here.
|
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
|
||||||
#[table_name = "community"]
|
#[table_name = "community"]
|
||||||
pub struct CommunityForm {
|
pub struct CommunityForm {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -36,7 +31,6 @@ pub struct CommunityForm {
|
||||||
pub category_id: i32,
|
pub category_id: i32,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub removed: Option<bool>,
|
pub removed: Option<bool>,
|
||||||
pub published: Option<chrono::NaiveDateTime>,
|
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: Option<bool>,
|
pub deleted: Option<bool>,
|
||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
|
@ -78,23 +72,20 @@ impl Crud<CommunityForm> for Community {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Community {
|
impl Community {
|
||||||
pub fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Self, Error> {
|
pub fn read_from_name(conn: &PgConnection, community_name: String) -> Result<Self, Error> {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
community
|
community
|
||||||
.filter(name.eq(community_name))
|
.filter(name.eq(community_name))
|
||||||
.first::<Self>(conn)
|
.first::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result<Self, Error> {
|
pub fn list(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
community
|
community.load::<Community>(conn)
|
||||||
.filter(actor_id.eq(community_id))
|
|
||||||
.first::<Self>(conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_local(conn: &PgConnection) -> Result<Vec<Self>, Error> {
|
pub fn get_url(&self) -> String {
|
||||||
use crate::schema::community::dsl::*;
|
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||||
community.filter(local.eq(true)).load::<Community>(conn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +207,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
|
||||||
.values(community_follower_form)
|
.values(community_follower_form)
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
fn unfollow(
|
fn ignore(
|
||||||
conn: &PgConnection,
|
conn: &PgConnection,
|
||||||
community_follower_form: &CommunityFollowerForm,
|
community_follower_form: &CommunityFollowerForm,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
|
@ -232,9 +223,8 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{super::user::*, *};
|
use super::super::user::*;
|
||||||
use crate::db::{establish_unpooled_connection, ListingType, SortType};
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_crud() {
|
fn test_crud() {
|
||||||
let conn = establish_unpooled_connection();
|
let conn = establish_unpooled_connection();
|
||||||
|
@ -256,7 +246,7 @@ mod tests {
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
show_avatars: true,
|
show_avatars: true,
|
||||||
send_notifications_to_email: false,
|
send_notifications_to_email: false,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
bio: None,
|
bio: None,
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
|
@ -276,12 +266,11 @@ mod tests {
|
||||||
removed: None,
|
removed: None,
|
||||||
deleted: None,
|
deleted: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -298,7 +287,7 @@ mod tests {
|
||||||
deleted: false,
|
deleted: false,
|
||||||
published: inserted_community.published,
|
published: inserted_community.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
actor_id: "http://fake.com".into(),
|
actor_id: "changeme".into(),
|
||||||
local: true,
|
local: true,
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: None,
|
public_key: None,
|
||||||
|
@ -352,7 +341,7 @@ mod tests {
|
||||||
let read_community = Community::read(&conn, inserted_community.id).unwrap();
|
let read_community = Community::read(&conn, inserted_community.id).unwrap();
|
||||||
let updated_community =
|
let updated_community =
|
||||||
Community::update(&conn, inserted_community.id, &new_community).unwrap();
|
Community::update(&conn, inserted_community.id, &new_community).unwrap();
|
||||||
let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
|
let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
|
||||||
let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
|
let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
|
||||||
let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
|
let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
|
||||||
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
|
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue